Repository files navigation

Self Hosted, self contained Rickroll container.

Docker PullsDocker Image Size (480p)Docker Image Size (720p)Docker Image Size (latest)Docker Image Size (2160p)Build StatusGitHub last commit

"Buy Me A Coffee"

More info can be found here.

This is a self-hosted Rickroll container. Point someone at it - a link, a QR code, whatever your heart desires - and they get properly rickrolled: full video and audio of Rick Astley. The video starts playing the instant the page loads; sound kicks in the moment they click anything at all, no matter what it is.

Image is based on nginxinc/nginx-unprivileged, runs as a non-root user, and everything needed to serve the video is baked into the image at build time - no external dependencies at runtime.

Quick start

docker run -d -p 8080:8080 --name rickroll modem7/docker-rickroll

Then visit http://localhost:8080 - see the Configuration example below for a docker-compose version.

Also published to GHCR if you'd rather pull from there: ghcr.io/modem7/docker-rickroll.

How it works

  • Every browser autoplays a muted video with zero restrictions, but every browser also actively refuses to let a page play sound without a genuine click/tap/keypress first - there's no trick or workaround for this, it's a deliberately and increasingly strictly enforced policy (the same reason YouTube and every other site with audio needs a click too). So the video autoplays muted immediately, and a decoy page state - a stuck-loading spinner or a fake "Something went wrong" site error, picked at random - entices that first click, which is all it takes to unmute. A fake cookie-consent banner sits on top of either one, since that's realistic regardless of what the rest of the page is doing.
  • The video keeps loading/playing muted in the background the whole time so it's instantly ready, but it's completely covered by the decoy until the reveal - nothing looks suspicious, and nothing gives it away early.
  • Only genuine clicks/taps/keypresses count for this - deliberately not mouse movement or scrolling, since browsers don't count those as real interaction either, and unmuting off one of those just gets the video paused by the browser's autoplay enforcement instead of actually unmuted.
  • The video defaults to object-fit: cover (fills the whole screen), but on a narrow/portrait viewport - basically any phone - that crops a 16:9 video down to a heavily zoomed-in sliver. Below a 1:1 aspect ratio it automatically switches to contain (letterboxed, but the whole frame is visible) instead, regardless of the OBJECT_FIT setting, since cover never looks right there. Desktop/landscape is untouched.
  • The video is served through nginx's mp4 module, so seeking/scrubbing and byte-range requests work properly and responses are cached.
  • The video isn't stored in git. It's fetched from a GitHub Release asset at build time and baked into the image, so the shipped container is still fully self-contained and works offline - git just doesn't carry the binary around.
  • Built for both linux/amd64 and linux/arm64/v8.

The decoy screens

These are the two decoy states mentioned above - one is shown at random on every visit, with the cookie banner on top of either one. Clicking or pressing a key anywhere unmutes and reveals the video.

LoadingError
Decoy loading screenDecoy error screen

These exist because of browser autoplay restrictions: browsers won't let a page play sound until there's been a genuine click/tap/keypress, so the video sits ready and muted in the background while one of these two screens is shown to fish for that first interaction. They're intentionally styled to look mundane or broken rather than like an obvious "click to play" button, since the whole point of the rickroll is the reveal catching you off guard. See #146 for a real report of this being mistaken for a compromise.

Container Screenshot

image

Tags

TagDescription
480pVideo starts automatically. 854x480.
720pVideo starts automatically. 1280x720.
latest / 1080pVideo starts automatically. 1920x1080 - latest and 1080p are the same image.
2160pVideo starts automatically. 3840x2160.

All tags are built from the same image - only the baked-in video resolution differs.

Environment Variables

VariableDescriptionDefault
PORTChanges the port nginx is listening on.8080
OVERLAYWhich page state(s) can entice the first click - a comma-separated list from error, loading, one is picked at random per visit. Set to a single value to always use just that one. A cookie-consent banner always shows on top regardless.both
TITLEBrowser tab title shown once the video is revealed (after the first click/keypress/etc).Rickroll
PRE_TITLEBrowser tab title shown before the video is revealed.Loading...
HEADLINEOptional heading rendered over the revealed video (e.g. a caption). Leave unset to omit it.(none)
HEIGHTCSS height of the video element.100vh
WIDTHCSS width of the video element.100%
OBJECT_FITCSS object-fit value for the video (cover, contain, etc).cover
LOOPWhether the video loops (true/false).true
VIDEO_FILEFilename of the video to serve, relative to the web root.video.mp4

Configuration example

services:
rickroll:
image: modem7/docker-rickrollcontainer_name: Rickrollports:
- 8080:8080

Build Arguments

The video is fetched pre-transcoded from a video asset attached to a GitHub Release at build time, rather than being stored in git. This only matters if you're building the image yourself - the published latest tag already has it baked in.

Build ArgDescriptionDefault
VIDEO_URLURL the build downloads the (already-transcoded) video from.video-assets-v1/video-1080p.mp4
# build the default (1080p, matches the published `latest` tag)
docker build -t rickroll:1080p .# point it at a different resolution asset instead
docker build --build-arg VIDEO_URL=https://github.com/modem7/docker-rickroll/releases/download/video-assets-v1/video-720p.mp4 -t rickroll:720p .

Transcoding (4K master -> 2160p/1080p/720p/480p mp4s) happens separately, via a manually-triggered GitHub Actions workflow that runs against the master video and uploads the results back to the Release. It only needs to run when the master video changes, not on every build.

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Self Hosted, self contained Rickroll container.

Docker PullsDocker Image Size (480p)Docker Image Size (720p)Docker Image Size (latest)Docker Image Size (2160p)Build StatusGitHub last commit

"Buy Me A Coffee"

More info can be found here.

This is a self-hosted Rickroll container. Point someone at it - a link, a QR code, whatever your heart desires - and they get properly rickrolled: full video and audio of Rick Astley. The video starts playing the instant the page loads; sound kicks in the moment they click anything at all, no matter what it is.

Image is based on nginxinc/nginx-unprivileged, runs as a non-root user, and everything needed to serve the video is baked into the image at build time - no external dependencies at runtime.

Quick start

docker run -d -p 8080:8080 --name rickroll modem7/docker-rickroll

Then visit http://localhost:8080 - see the Configuration example below for a docker-compose version.

Also published to GHCR if you'd rather pull from there: ghcr.io/modem7/docker-rickroll.

How it works

  • Every browser autoplays a muted video with zero restrictions, but every browser also actively refuses to let a page play sound without a genuine click/tap/keypress first - there's no trick or workaround for this, it's a deliberately and increasingly strictly enforced policy (the same reason YouTube and every other site with audio needs a click too). So the video autoplays muted immediately, and a decoy page state - a stuck-loading spinner or a fake "Something went wrong" site error, picked at random - entices that first click, which is all it takes to unmute. A fake cookie-consent banner sits on top of either one, since that's realistic regardless of what the rest of the page is doing.
  • The video keeps loading/playing muted in the background the whole time so it's instantly ready, but it's completely covered by the decoy until the reveal - nothing looks suspicious, and nothing gives it away early.
  • Only genuine clicks/taps/keypresses count for this - deliberately not mouse movement or scrolling, since browsers don't count those as real interaction either, and unmuting off one of those just gets the video paused by the browser's autoplay enforcement instead of actually unmuted.
  • The video defaults to object-fit: cover (fills the whole screen), but on a narrow/portrait viewport - basically any phone - that crops a 16:9 video down to a heavily zoomed-in sliver. Below a 1:1 aspect ratio it automatically switches to contain (letterboxed, but the whole frame is visible) instead, regardless of the OBJECT_FIT setting, since cover never looks right there. Desktop/landscape is untouched.
  • The video is served through nginx's mp4 module, so seeking/scrubbing and byte-range requests work properly and responses are cached.
  • The video isn't stored in git. It's fetched from a GitHub Release asset at build time and baked into the image, so the shipped container is still fully self-contained and works offline - git just doesn't carry the binary around.
  • Built for both linux/amd64 and linux/arm64/v8.

The decoy screens

These are the two decoy states mentioned above - one is shown at random on every visit, with the cookie banner on top of either one. Clicking or pressing a key anywhere unmutes and reveals the video.

LoadingError
Decoy loading screenDecoy error screen

These exist because of browser autoplay restrictions: browsers won't let a page play sound until there's been a genuine click/tap/keypress, so the video sits ready and muted in the background while one of these two screens is shown to fish for that first interaction. They're intentionally styled to look mundane or broken rather than like an obvious "click to play" button, since the whole point of the rickroll is the reveal catching you off guard. See #146 for a real report of this being mistaken for a compromise.

Container Screenshot

image

Tags

TagDescription
480pVideo starts automatically. 854x480.
720pVideo starts automatically. 1280x720.
latest / 1080pVideo starts automatically. 1920x1080 - latest and 1080p are the same image.
2160pVideo starts automatically. 3840x2160.

All tags are built from the same image - only the baked-in video resolution differs.

Environment Variables

VariableDescriptionDefault
PORTChanges the port nginx is listening on.8080
OVERLAYWhich page state(s) can entice the first click - a comma-separated list from error, loading, one is picked at random per visit. Set to a single value to always use just that one. A cookie-consent banner always shows on top regardless.both
TITLEBrowser tab title shown once the video is revealed (after the first click/keypress/etc).Rickroll
PRE_TITLEBrowser tab title shown before the video is revealed.Loading...
HEADLINEOptional heading rendered over the revealed video (e.g. a caption). Leave unset to omit it.(none)
HEIGHTCSS height of the video element.100vh
WIDTHCSS width of the video element.100%
OBJECT_FITCSS object-fit value for the video (cover, contain, etc).cover
LOOPWhether the video loops (true/false).true
VIDEO_FILEFilename of the video to serve, relative to the web root.video.mp4

Configuration example

services:
rickroll:
image: modem7/docker-rickrollcontainer_name: Rickrollports:
- 8080:8080

Build Arguments

The video is fetched pre-transcoded from a video asset attached to a GitHub Release at build time, rather than being stored in git. This only matters if you're building the image yourself - the published latest tag already has it baked in.

Build ArgDescriptionDefault
VIDEO_URLURL the build downloads the (already-transcoded) video from.video-assets-v1/video-1080p.mp4
# build the default (1080p, matches the published `latest` tag)
docker build -t rickroll:1080p .# point it at a different resolution asset instead
docker build --build-arg VIDEO_URL=https://github.com/modem7/docker-rickroll/releases/download/video-assets-v1/video-720p.mp4 -t rickroll:720p .

Transcoding (4K master -> 2160p/1080p/720p/480p mp4s) happens separately, via a manually-triggered GitHub Actions workflow that runs against the master video and uploads the results back to the Release. It only needs to run when the master video changes, not on every build.

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Self Hosted, self contained Rickroll container.

Docker PullsDocker Image Size (480p)Docker Image Size (720p)Docker Image Size (latest)Docker Image Size (2160p)Build StatusGitHub last commit

"Buy Me A Coffee"

More info can be found here.

This is a self-hosted Rickroll container. Point someone at it - a link, a QR code, whatever your heart desires - and they get properly rickrolled: full video and audio of Rick Astley. The video starts playing the instant the page loads; sound kicks in the moment they click anything at all, no matter what it is.

Image is based on nginxinc/nginx-unprivileged, runs as a non-root user, and everything needed to serve the video is baked into the image at build time - no external dependencies at runtime.

Quick start

docker run -d -p 8080:8080 --name rickroll modem7/docker-rickroll

Then visit http://localhost:8080 - see the Configuration example below for a docker-compose version.

Also published to GHCR if you'd rather pull from there: ghcr.io/modem7/docker-rickroll.

How it works

  • Every browser autoplays a muted video with zero restrictions, but every browser also actively refuses to let a page play sound without a genuine click/tap/keypress first - there's no trick or workaround for this, it's a deliberately and increasingly strictly enforced policy (the same reason YouTube and every other site with audio needs a click too). So the video autoplays muted immediately, and a decoy page state - a stuck-loading spinner or a fake "Something went wrong" site error, picked at random - entices that first click, which is all it takes to unmute. A fake cookie-consent banner sits on top of either one, since that's realistic regardless of what the rest of the page is doing.
  • The video keeps loading/playing muted in the background the whole time so it's instantly ready, but it's completely covered by the decoy until the reveal - nothing looks suspicious, and nothing gives it away early.
  • Only genuine clicks/taps/keypresses count for this - deliberately not mouse movement or scrolling, since browsers don't count those as real interaction either, and unmuting off one of those just gets the video paused by the browser's autoplay enforcement instead of actually unmuted.
  • The video defaults to object-fit: cover (fills the whole screen), but on a narrow/portrait viewport - basically any phone - that crops a 16:9 video down to a heavily zoomed-in sliver. Below a 1:1 aspect ratio it automatically switches to contain (letterboxed, but the whole frame is visible) instead, regardless of the OBJECT_FIT setting, since cover never looks right there. Desktop/landscape is untouched.
  • The video is served through nginx's mp4 module, so seeking/scrubbing and byte-range requests work properly and responses are cached.
  • The video isn't stored in git. It's fetched from a GitHub Release asset at build time and baked into the image, so the shipped container is still fully self-contained and works offline - git just doesn't carry the binary around.
  • Built for both linux/amd64 and linux/arm64/v8.

The decoy screens

These are the two decoy states mentioned above - one is shown at random on every visit, with the cookie banner on top of either one. Clicking or pressing a key anywhere unmutes and reveals the video.

LoadingError
Decoy loading screenDecoy error screen

These exist because of browser autoplay restrictions: browsers won't let a page play sound until there's been a genuine click/tap/keypress, so the video sits ready and muted in the background while one of these two screens is shown to fish for that first interaction. They're intentionally styled to look mundane or broken rather than like an obvious "click to play" button, since the whole point of the rickroll is the reveal catching you off guard. See #146 for a real report of this being mistaken for a compromise.

Container Screenshot

image

Tags

TagDescription
480pVideo starts automatically. 854x480.
720pVideo starts automatically. 1280x720.
latest / 1080pVideo starts automatically. 1920x1080 - latest and 1080p are the same image.
2160pVideo starts automatically. 3840x2160.

All tags are built from the same image - only the baked-in video resolution differs.

Environment Variables

VariableDescriptionDefault
PORTChanges the port nginx is listening on.8080
OVERLAYWhich page state(s) can entice the first click - a comma-separated list from error, loading, one is picked at random per visit. Set to a single value to always use just that one. A cookie-consent banner always shows on top regardless.both
TITLEBrowser tab title shown once the video is revealed (after the first click/keypress/etc).Rickroll
PRE_TITLEBrowser tab title shown before the video is revealed.Loading...
HEADLINEOptional heading rendered over the revealed video (e.g. a caption). Leave unset to omit it.(none)
HEIGHTCSS height of the video element.100vh
WIDTHCSS width of the video element.100%
OBJECT_FITCSS object-fit value for the video (cover, contain, etc).cover
LOOPWhether the video loops (true/false).true
VIDEO_FILEFilename of the video to serve, relative to the web root.video.mp4

Configuration example

services:
rickroll:
image: modem7/docker-rickrollcontainer_name: Rickrollports:
- 8080:8080

Build Arguments

The video is fetched pre-transcoded from a video asset attached to a GitHub Release at build time, rather than being stored in git. This only matters if you're building the image yourself - the published latest tag already has it baked in.

Build ArgDescriptionDefault
VIDEO_URLURL the build downloads the (already-transcoded) video from.video-assets-v1/video-1080p.mp4
# build the default (1080p, matches the published `latest` tag)
docker build -t rickroll:1080p .# point it at a different resolution asset instead
docker build --build-arg VIDEO_URL=https://github.com/modem7/docker-rickroll/releases/download/video-assets-v1/video-720p.mp4 -t rickroll:720p .

Transcoding (4K master -> 2160p/1080p/720p/480p mp4s) happens separately, via a manually-triggered GitHub Actions workflow that runs against the master video and uploads the results back to the Release. It only needs to run when the master video changes, not on every build.

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Self Hosted, self contained Rickroll container.

Docker PullsDocker Image Size (480p)Docker Image Size (720p)Docker Image Size (latest)Docker Image Size (2160p)Build StatusGitHub last commit

"Buy Me A Coffee"

More info can be found here.

This is a self-hosted Rickroll container. Point someone at it - a link, a QR code, whatever your heart desires - and they get properly rickrolled: full video and audio of Rick Astley. The video starts playing the instant the page loads; sound kicks in the moment they click anything at all, no matter what it is.

Image is based on nginxinc/nginx-unprivileged, runs as a non-root user, and everything needed to serve the video is baked into the image at build time - no external dependencies at runtime.

Quick start

docker run -d -p 8080:8080 --name rickroll modem7/docker-rickroll

Then visit http://localhost:8080 - see the Configuration example below for a docker-compose version.

Also published to GHCR if you'd rather pull from there: ghcr.io/modem7/docker-rickroll.

How it works

  • Every browser autoplays a muted video with zero restrictions, but every browser also actively refuses to let a page play sound without a genuine click/tap/keypress first - there's no trick or workaround for this, it's a deliberately and increasingly strictly enforced policy (the same reason YouTube and every other site with audio needs a click too). So the video autoplays muted immediately, and a decoy page state - a stuck-loading spinner or a fake "Something went wrong" site error, picked at random - entices that first click, which is all it takes to unmute. A fake cookie-consent banner sits on top of either one, since that's realistic regardless of what the rest of the page is doing.
  • The video keeps loading/playing muted in the background the whole time so it's instantly ready, but it's completely covered by the decoy until the reveal - nothing looks suspicious, and nothing gives it away early.
  • Only genuine clicks/taps/keypresses count for this - deliberately not mouse movement or scrolling, since browsers don't count those as real interaction either, and unmuting off one of those just gets the video paused by the browser's autoplay enforcement instead of actually unmuted.
  • The video defaults to object-fit: cover (fills the whole screen), but on a narrow/portrait viewport - basically any phone - that crops a 16:9 video down to a heavily zoomed-in sliver. Below a 1:1 aspect ratio it automatically switches to contain (letterboxed, but the whole frame is visible) instead, regardless of the OBJECT_FIT setting, since cover never looks right there. Desktop/landscape is untouched.
  • The video is served through nginx's mp4 module, so seeking/scrubbing and byte-range requests work properly and responses are cached.
  • The video isn't stored in git. It's fetched from a GitHub Release asset at build time and baked into the image, so the shipped container is still fully self-contained and works offline - git just doesn't carry the binary around.
  • Built for both linux/amd64 and linux/arm64/v8.

The decoy screens

These are the two decoy states mentioned above - one is shown at random on every visit, with the cookie banner on top of either one. Clicking or pressing a key anywhere unmutes and reveals the video.

LoadingError
Decoy loading screenDecoy error screen

These exist because of browser autoplay restrictions: browsers won't let a page play sound until there's been a genuine click/tap/keypress, so the video sits ready and muted in the background while one of these two screens is shown to fish for that first interaction. They're intentionally styled to look mundane or broken rather than like an obvious "click to play" button, since the whole point of the rickroll is the reveal catching you off guard. See #146 for a real report of this being mistaken for a compromise.

Container Screenshot

image

Tags

TagDescription
480pVideo starts automatically. 854x480.
720pVideo starts automatically. 1280x720.
latest / 1080pVideo starts automatically. 1920x1080 - latest and 1080p are the same image.
2160pVideo starts automatically. 3840x2160.

All tags are built from the same image - only the baked-in video resolution differs.

Environment Variables

VariableDescriptionDefault
PORTChanges the port nginx is listening on.8080
OVERLAYWhich page state(s) can entice the first click - a comma-separated list from error, loading, one is picked at random per visit. Set to a single value to always use just that one. A cookie-consent banner always shows on top regardless.both
TITLEBrowser tab title shown once the video is revealed (after the first click/keypress/etc).Rickroll
PRE_TITLEBrowser tab title shown before the video is revealed.Loading...
HEADLINEOptional heading rendered over the revealed video (e.g. a caption). Leave unset to omit it.(none)
HEIGHTCSS height of the video element.100vh
WIDTHCSS width of the video element.100%
OBJECT_FITCSS object-fit value for the video (cover, contain, etc).cover
LOOPWhether the video loops (true/false).true
VIDEO_FILEFilename of the video to serve, relative to the web root.video.mp4

Configuration example

services:
rickroll:
image: modem7/docker-rickrollcontainer_name: Rickrollports:
- 8080:8080

Build Arguments

The video is fetched pre-transcoded from a video asset attached to a GitHub Release at build time, rather than being stored in git. This only matters if you're building the image yourself - the published latest tag already has it baked in.

Build ArgDescriptionDefault
VIDEO_URLURL the build downloads the (already-transcoded) video from.video-assets-v1/video-1080p.mp4
# build the default (1080p, matches the published `latest` tag)
docker build -t rickroll:1080p .# point it at a different resolution asset instead
docker build --build-arg VIDEO_URL=https://github.com/modem7/docker-rickroll/releases/download/video-assets-v1/video-720p.mp4 -t rickroll:720p .

Transcoding (4K master -> 2160p/1080p/720p/480p mp4s) happens separately, via a manually-triggered GitHub Actions workflow that runs against the master video and uploads the results back to the Release. It only needs to run when the master video changes, not on every build.

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Self Hosted, self contained Rickroll container.

Docker PullsDocker Image Size (480p)Docker Image Size (720p)Docker Image Size (latest)Docker Image Size (2160p)Build StatusGitHub last commit

"Buy Me A Coffee"

More info can be found here.

This is a self-hosted Rickroll container. Point someone at it - a link, a QR code, whatever your heart desires - and they get properly rickrolled: full video and audio of Rick Astley. The video starts playing the instant the page loads; sound kicks in the moment they click anything at all, no matter what it is.

Image is based on nginxinc/nginx-unprivileged, runs as a non-root user, and everything needed to serve the video is baked into the image at build time - no external dependencies at runtime.

Quick start

docker run -d -p 8080:8080 --name rickroll modem7/docker-rickroll

Then visit http://localhost:8080 - see the Configuration example below for a docker-compose version.

Also published to GHCR if you'd rather pull from there: ghcr.io/modem7/docker-rickroll.

How it works

  • Every browser autoplays a muted video with zero restrictions, but every browser also actively refuses to let a page play sound without a genuine click/tap/keypress first - there's no trick or workaround for this, it's a deliberately and increasingly strictly enforced policy (the same reason YouTube and every other site with audio needs a click too). So the video autoplays muted immediately, and a decoy page state - a stuck-loading spinner or a fake "Something went wrong" site error, picked at random - entices that first click, which is all it takes to unmute. A fake cookie-consent banner sits on top of either one, since that's realistic regardless of what the rest of the page is doing.
  • The video keeps loading/playing muted in the background the whole time so it's instantly ready, but it's completely covered by the decoy until the reveal - nothing looks suspicious, and nothing gives it away early.
  • Only genuine clicks/taps/keypresses count for this - deliberately not mouse movement or scrolling, since browsers don't count those as real interaction either, and unmuting off one of those just gets the video paused by the browser's autoplay enforcement instead of actually unmuted.
  • The video defaults to object-fit: cover (fills the whole screen), but on a narrow/portrait viewport - basically any phone - that crops a 16:9 video down to a heavily zoomed-in sliver. Below a 1:1 aspect ratio it automatically switches to contain (letterboxed, but the whole frame is visible) instead, regardless of the OBJECT_FIT setting, since cover never looks right there. Desktop/landscape is untouched.
  • The video is served through nginx's mp4 module, so seeking/scrubbing and byte-range requests work properly and responses are cached.
  • The video isn't stored in git. It's fetched from a GitHub Release asset at build time and baked into the image, so the shipped container is still fully self-contained and works offline - git just doesn't carry the binary around.
  • Built for both linux/amd64 and linux/arm64/v8.

The decoy screens

These are the two decoy states mentioned above - one is shown at random on every visit, with the cookie banner on top of either one. Clicking or pressing a key anywhere unmutes and reveals the video.

LoadingError
Decoy loading screenDecoy error screen

These exist because of browser autoplay restrictions: browsers won't let a page play sound until there's been a genuine click/tap/keypress, so the video sits ready and muted in the background while one of these two screens is shown to fish for that first interaction. They're intentionally styled to look mundane or broken rather than like an obvious "click to play" button, since the whole point of the rickroll is the reveal catching you off guard. See #146 for a real report of this being mistaken for a compromise.

Container Screenshot

image

Tags

TagDescription
480pVideo starts automatically. 854x480.
720pVideo starts automatically. 1280x720.
latest / 1080pVideo starts automatically. 1920x1080 - latest and 1080p are the same image.
2160pVideo starts automatically. 3840x2160.

All tags are built from the same image - only the baked-in video resolution differs.

Environment Variables

VariableDescriptionDefault
PORTChanges the port nginx is listening on.8080
OVERLAYWhich page state(s) can entice the first click - a comma-separated list from error, loading, one is picked at random per visit. Set to a single value to always use just that one. A cookie-consent banner always shows on top regardless.both
TITLEBrowser tab title shown once the video is revealed (after the first click/keypress/etc).Rickroll
PRE_TITLEBrowser tab title shown before the video is revealed.Loading...
HEADLINEOptional heading rendered over the revealed video (e.g. a caption). Leave unset to omit it.(none)
HEIGHTCSS height of the video element.100vh
WIDTHCSS width of the video element.100%
OBJECT_FITCSS object-fit value for the video (cover, contain, etc).cover
LOOPWhether the video loops (true/false).true
VIDEO_FILEFilename of the video to serve, relative to the web root.video.mp4

Configuration example

services:
rickroll:
image: modem7/docker-rickrollcontainer_name: Rickrollports:
- 8080:8080

Build Arguments

The video is fetched pre-transcoded from a video asset attached to a GitHub Release at build time, rather than being stored in git. This only matters if you're building the image yourself - the published latest tag already has it baked in.

Build ArgDescriptionDefault
VIDEO_URLURL the build downloads the (already-transcoded) video from.video-assets-v1/video-1080p.mp4
# build the default (1080p, matches the published `latest` tag)
docker build -t rickroll:1080p .# point it at a different resolution asset instead
docker build --build-arg VIDEO_URL=https://github.com/modem7/docker-rickroll/releases/download/video-assets-v1/video-720p.mp4 -t rickroll:720p .

Transcoding (4K master -> 2160p/1080p/720p/480p mp4s) happens separately, via a manually-triggered GitHub Actions workflow that runs against the master video and uploads the results back to the Release. It only needs to run when the master video changes, not on every build.

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Self Hosted, self contained Rickroll container.

Docker PullsDocker Image Size (480p)Docker Image Size (720p)Docker Image Size (latest)Docker Image Size (2160p)Build StatusGitHub last commit

"Buy Me A Coffee"

More info can be found here.

This is a self-hosted Rickroll container. Point someone at it - a link, a QR code, whatever your heart desires - and they get properly rickrolled: full video and audio of Rick Astley. The video starts playing the instant the page loads; sound kicks in the moment they click anything at all, no matter what it is.

Image is based on nginxinc/nginx-unprivileged, runs as a non-root user, and everything needed to serve the video is baked into the image at build time - no external dependencies at runtime.

Quick start

docker run -d -p 8080:8080 --name rickroll modem7/docker-rickroll

Then visit http://localhost:8080 - see the Configuration example below for a docker-compose version.

Also published to GHCR if you'd rather pull from there: ghcr.io/modem7/docker-rickroll.

How it works

  • Every browser autoplays a muted video with zero restrictions, but every browser also actively refuses to let a page play sound without a genuine click/tap/keypress first - there's no trick or workaround for this, it's a deliberately and increasingly strictly enforced policy (the same reason YouTube and every other site with audio needs a click too). So the video autoplays muted immediately, and a decoy page state - a stuck-loading spinner or a fake "Something went wrong" site error, picked at random - entices that first click, which is all it takes to unmute. A fake cookie-consent banner sits on top of either one, since that's realistic regardless of what the rest of the page is doing.
  • The video keeps loading/playing muted in the background the whole time so it's instantly ready, but it's completely covered by the decoy until the reveal - nothing looks suspicious, and nothing gives it away early.
  • Only genuine clicks/taps/keypresses count for this - deliberately not mouse movement or scrolling, since browsers don't count those as real interaction either, and unmuting off one of those just gets the video paused by the browser's autoplay enforcement instead of actually unmuted.
  • The video defaults to object-fit: cover (fills the whole screen), but on a narrow/portrait viewport - basically any phone - that crops a 16:9 video down to a heavily zoomed-in sliver. Below a 1:1 aspect ratio it automatically switches to contain (letterboxed, but the whole frame is visible) instead, regardless of the OBJECT_FIT setting, since cover never looks right there. Desktop/landscape is untouched.
  • The video is served through nginx's mp4 module, so seeking/scrubbing and byte-range requests work properly and responses are cached.
  • The video isn't stored in git. It's fetched from a GitHub Release asset at build time and baked into the image, so the shipped container is still fully self-contained and works offline - git just doesn't carry the binary around.
  • Built for both linux/amd64 and linux/arm64/v8.

The decoy screens

These are the two decoy states mentioned above - one is shown at random on every visit, with the cookie banner on top of either one. Clicking or pressing a key anywhere unmutes and reveals the video.

LoadingError
Decoy loading screenDecoy error screen

These exist because of browser autoplay restrictions: browsers won't let a page play sound until there's been a genuine click/tap/keypress, so the video sits ready and muted in the background while one of these two screens is shown to fish for that first interaction. They're intentionally styled to look mundane or broken rather than like an obvious "click to play" button, since the whole point of the rickroll is the reveal catching you off guard. See #146 for a real report of this being mistaken for a compromise.

Container Screenshot

image

Tags

TagDescription
480pVideo starts automatically. 854x480.
720pVideo starts automatically. 1280x720.
latest / 1080pVideo starts automatically. 1920x1080 - latest and 1080p are the same image.
2160pVideo starts automatically. 3840x2160.

All tags are built from the same image - only the baked-in video resolution differs.

Environment Variables

VariableDescriptionDefault
PORTChanges the port nginx is listening on.8080
OVERLAYWhich page state(s) can entice the first click - a comma-separated list from error, loading, one is picked at random per visit. Set to a single value to always use just that one. A cookie-consent banner always shows on top regardless.both
TITLEBrowser tab title shown once the video is revealed (after the first click/keypress/etc).Rickroll
PRE_TITLEBrowser tab title shown before the video is revealed.Loading...
HEADLINEOptional heading rendered over the revealed video (e.g. a caption). Leave unset to omit it.(none)
HEIGHTCSS height of the video element.100vh
WIDTHCSS width of the video element.100%
OBJECT_FITCSS object-fit value for the video (cover, contain, etc).cover
LOOPWhether the video loops (true/false).true
VIDEO_FILEFilename of the video to serve, relative to the web root.video.mp4

Configuration example

services:
rickroll:
image: modem7/docker-rickrollcontainer_name: Rickrollports:
- 8080:8080

Build Arguments

The video is fetched pre-transcoded from a video asset attached to a GitHub Release at build time, rather than being stored in git. This only matters if you're building the image yourself - the published latest tag already has it baked in.

Build ArgDescriptionDefault
VIDEO_URLURL the build downloads the (already-transcoded) video from.video-assets-v1/video-1080p.mp4
# build the default (1080p, matches the published `latest` tag)
docker build -t rickroll:1080p .# point it at a different resolution asset instead
docker build --build-arg VIDEO_URL=https://github.com/modem7/docker-rickroll/releases/download/video-assets-v1/video-720p.mp4 -t rickroll:720p .

Transcoding (4K master -> 2160p/1080p/720p/480p mp4s) happens separately, via a manually-triggered GitHub Actions workflow that runs against the master video and uploads the results back to the Release. It only needs to run when the master video changes, not on every build.

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Self Hosted, self contained Rickroll container.

Docker PullsDocker Image Size (480p)Docker Image Size (720p)Docker Image Size (latest)Docker Image Size (2160p)Build StatusGitHub last commit

"Buy Me A Coffee"

More info can be found here.

This is a self-hosted Rickroll container. Point someone at it - a link, a QR code, whatever your heart desires - and they get properly rickrolled: full video and audio of Rick Astley. The video starts playing the instant the page loads; sound kicks in the moment they click anything at all, no matter what it is.

Image is based on nginxinc/nginx-unprivileged, runs as a non-root user, and everything needed to serve the video is baked into the image at build time - no external dependencies at runtime.

Quick start

docker run -d -p 8080:8080 --name rickroll modem7/docker-rickroll

Then visit http://localhost:8080 - see the Configuration example below for a docker-compose version.

Also published to GHCR if you'd rather pull from there: ghcr.io/modem7/docker-rickroll.

How it works

  • Every browser autoplays a muted video with zero restrictions, but every browser also actively refuses to let a page play sound without a genuine click/tap/keypress first - there's no trick or workaround for this, it's a deliberately and increasingly strictly enforced policy (the same reason YouTube and every other site with audio needs a click too). So the video autoplays muted immediately, and a decoy page state - a stuck-loading spinner or a fake "Something went wrong" site error, picked at random - entices that first click, which is all it takes to unmute. A fake cookie-consent banner sits on top of either one, since that's realistic regardless of what the rest of the page is doing.
  • The video keeps loading/playing muted in the background the whole time so it's instantly ready, but it's completely covered by the decoy until the reveal - nothing looks suspicious, and nothing gives it away early.
  • Only genuine clicks/taps/keypresses count for this - deliberately not mouse movement or scrolling, since browsers don't count those as real interaction either, and unmuting off one of those just gets the video paused by the browser's autoplay enforcement instead of actually unmuted.
  • The video defaults to object-fit: cover (fills the whole screen), but on a narrow/portrait viewport - basically any phone - that crops a 16:9 video down to a heavily zoomed-in sliver. Below a 1:1 aspect ratio it automatically switches to contain (letterboxed, but the whole frame is visible) instead, regardless of the OBJECT_FIT setting, since cover never looks right there. Desktop/landscape is untouched.
  • The video is served through nginx's mp4 module, so seeking/scrubbing and byte-range requests work properly and responses are cached.
  • The video isn't stored in git. It's fetched from a GitHub Release asset at build time and baked into the image, so the shipped container is still fully self-contained and works offline - git just doesn't carry the binary around.
  • Built for both linux/amd64 and linux/arm64/v8.

The decoy screens

These are the two decoy states mentioned above - one is shown at random on every visit, with the cookie banner on top of either one. Clicking or pressing a key anywhere unmutes and reveals the video.

LoadingError
Decoy loading screenDecoy error screen

These exist because of browser autoplay restrictions: browsers won't let a page play sound until there's been a genuine click/tap/keypress, so the video sits ready and muted in the background while one of these two screens is shown to fish for that first interaction. They're intentionally styled to look mundane or broken rather than like an obvious "click to play" button, since the whole point of the rickroll is the reveal catching you off guard. See #146 for a real report of this being mistaken for a compromise.

Container Screenshot

image

Tags

TagDescription
480pVideo starts automatically. 854x480.
720pVideo starts automatically. 1280x720.
latest / 1080pVideo starts automatically. 1920x1080 - latest and 1080p are the same image.
2160pVideo starts automatically. 3840x2160.

All tags are built from the same image - only the baked-in video resolution differs.

Environment Variables

VariableDescriptionDefault
PORTChanges the port nginx is listening on.8080
OVERLAYWhich page state(s) can entice the first click - a comma-separated list from error, loading, one is picked at random per visit. Set to a single value to always use just that one. A cookie-consent banner always shows on top regardless.both
TITLEBrowser tab title shown once the video is revealed (after the first click/keypress/etc).Rickroll
PRE_TITLEBrowser tab title shown before the video is revealed.Loading...
HEADLINEOptional heading rendered over the revealed video (e.g. a caption). Leave unset to omit it.(none)
HEIGHTCSS height of the video element.100vh
WIDTHCSS width of the video element.100%
OBJECT_FITCSS object-fit value for the video (cover, contain, etc).cover
LOOPWhether the video loops (true/false).true
VIDEO_FILEFilename of the video to serve, relative to the web root.video.mp4

Configuration example

services:
rickroll:
image: modem7/docker-rickrollcontainer_name: Rickrollports:
- 8080:8080

Build Arguments

The video is fetched pre-transcoded from a video asset attached to a GitHub Release at build time, rather than being stored in git. This only matters if you're building the image yourself - the published latest tag already has it baked in.

Build ArgDescriptionDefault
VIDEO_URLURL the build downloads the (already-transcoded) video from.video-assets-v1/video-1080p.mp4
# build the default (1080p, matches the published `latest` tag)
docker build -t rickroll:1080p .# point it at a different resolution asset instead
docker build --build-arg VIDEO_URL=https://github.com/modem7/docker-rickroll/releases/download/video-assets-v1/video-720p.mp4 -t rickroll:720p .

Transcoding (4K master -> 2160p/1080p/720p/480p mp4s) happens separately, via a manually-triggered GitHub Actions workflow that runs against the master video and uploads the results back to the Release. It only needs to run when the master video changes, not on every build.

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Self Hosted, self contained Rickroll container.

Docker PullsDocker Image Size (480p)Docker Image Size (720p)Docker Image Size (latest)Docker Image Size (2160p)Build StatusGitHub last commit

"Buy Me A Coffee"

More info can be found here.

This is a self-hosted Rickroll container. Point someone at it - a link, a QR code, whatever your heart desires - and they get properly rickrolled: full video and audio of Rick Astley. The video starts playing the instant the page loads; sound kicks in the moment they click anything at all, no matter what it is.

Image is based on nginxinc/nginx-unprivileged, runs as a non-root user, and everything needed to serve the video is baked into the image at build time - no external dependencies at runtime.

Quick start

docker run -d -p 8080:8080 --name rickroll modem7/docker-rickroll

Then visit http://localhost:8080 - see the Configuration example below for a docker-compose version.

Also published to GHCR if you'd rather pull from there: ghcr.io/modem7/docker-rickroll.

How it works

  • Every browser autoplays a muted video with zero restrictions, but every browser also actively refuses to let a page play sound without a genuine click/tap/keypress first - there's no trick or workaround for this, it's a deliberately and increasingly strictly enforced policy (the same reason YouTube and every other site with audio needs a click too). So the video autoplays muted immediately, and a decoy page state - a stuck-loading spinner or a fake "Something went wrong" site error, picked at random - entices that first click, which is all it takes to unmute. A fake cookie-consent banner sits on top of either one, since that's realistic regardless of what the rest of the page is doing.
  • The video keeps loading/playing muted in the background the whole time so it's instantly ready, but it's completely covered by the decoy until the reveal - nothing looks suspicious, and nothing gives it away early.
  • Only genuine clicks/taps/keypresses count for this - deliberately not mouse movement or scrolling, since browsers don't count those as real interaction either, and unmuting off one of those just gets the video paused by the browser's autoplay enforcement instead of actually unmuted.
  • The video defaults to object-fit: cover (fills the whole screen), but on a narrow/portrait viewport - basically any phone - that crops a 16:9 video down to a heavily zoomed-in sliver. Below a 1:1 aspect ratio it automatically switches to contain (letterboxed, but the whole frame is visible) instead, regardless of the OBJECT_FIT setting, since cover never looks right there. Desktop/landscape is untouched.
  • The video is served through nginx's mp4 module, so seeking/scrubbing and byte-range requests work properly and responses are cached.
  • The video isn't stored in git. It's fetched from a GitHub Release asset at build time and baked into the image, so the shipped container is still fully self-contained and works offline - git just doesn't carry the binary around.
  • Built for both linux/amd64 and linux/arm64/v8.

The decoy screens

These are the two decoy states mentioned above - one is shown at random on every visit, with the cookie banner on top of either one. Clicking or pressing a key anywhere unmutes and reveals the video.

LoadingError
Decoy loading screenDecoy error screen

These exist because of browser autoplay restrictions: browsers won't let a page play sound until there's been a genuine click/tap/keypress, so the video sits ready and muted in the background while one of these two screens is shown to fish for that first interaction. They're intentionally styled to look mundane or broken rather than like an obvious "click to play" button, since the whole point of the rickroll is the reveal catching you off guard. See #146 for a real report of this being mistaken for a compromise.

Container Screenshot

image

Tags

TagDescription
480pVideo starts automatically. 854x480.
720pVideo starts automatically. 1280x720.
latest / 1080pVideo starts automatically. 1920x1080 - latest and 1080p are the same image.
2160pVideo starts automatically. 3840x2160.

All tags are built from the same image - only the baked-in video resolution differs.

Environment Variables

VariableDescriptionDefault
PORTChanges the port nginx is listening on.8080
OVERLAYWhich page state(s) can entice the first click - a comma-separated list from error, loading, one is picked at random per visit. Set to a single value to always use just that one. A cookie-consent banner always shows on top regardless.both
TITLEBrowser tab title shown once the video is revealed (after the first click/keypress/etc).Rickroll
PRE_TITLEBrowser tab title shown before the video is revealed.Loading...
HEADLINEOptional heading rendered over the revealed video (e.g. a caption). Leave unset to omit it.(none)
HEIGHTCSS height of the video element.100vh
WIDTHCSS width of the video element.100%
OBJECT_FITCSS object-fit value for the video (cover, contain, etc).cover
LOOPWhether the video loops (true/false).true
VIDEO_FILEFilename of the video to serve, relative to the web root.video.mp4

Configuration example

services:
rickroll:
image: modem7/docker-rickrollcontainer_name: Rickrollports:
- 8080:8080

Build Arguments

The video is fetched pre-transcoded from a video asset attached to a GitHub Release at build time, rather than being stored in git. This only matters if you're building the image yourself - the published latest tag already has it baked in.

Build ArgDescriptionDefault
VIDEO_URLURL the build downloads the (already-transcoded) video from.video-assets-v1/video-1080p.mp4
# build the default (1080p, matches the published `latest` tag)
docker build -t rickroll:1080p .# point it at a different resolution asset instead
docker build --build-arg VIDEO_URL=https://github.com/modem7/docker-rickroll/releases/download/video-assets-v1/video-720p.mp4 -t rickroll:720p .

Transcoding (4K master -> 2160p/1080p/720p/480p mp4s) happens separately, via a manually-triggered GitHub Actions workflow that runs against the master video and uploads the results back to the Release. It only needs to run when the master video changes, not on every build.

Releases

Packages

Used by

Contributors

Languages