
This article provides a guide demonstrating how to deploy Invidious on Ubuntu VPS.
What is Invidious?
Invidious is a free, open-source alternative front end for YouTube.
It lets users browse, search, and watch YouTube content through a separate website instead of using YouTubeās official interface. Depending on the instanceās configuration, it can offer:
- Fewer trackers and less JavaScript
- No Google account requirement
- Subscriptions and playlists stored locally
- RSS feeds
- Audio-only playback
- A lighter interface
- An API for developers
Invidious does not host the videos itself. It retrieves public information and media from YouTube, so it can stop working when YouTube changes its systems or blocks an instance.
An Invidious instance is simply a server running the Invidious software. You can use a public instance or host your own for greater control and privacy.
Overview
This guide deploys Invidious on an Ubuntu VPS using:
- Docker Engine and Docker Compose
- PostgreSQL
- Invidious Companion
- NGINX as a reverse proxy
- A free Letās Encrypt TLS certificate
- UFW firewall rules
- Persistent Docker volumes
The finished instance will be available at a domain such as:
https://invidious.example.com
This guide assumes Ubuntu 22.04 LTS or Ubuntu 24.04 LTS and a user account with sudo privileges. Docker currently supports both releases.
Check the VPS requirements
For a small private instance, start with approximately:
- 2 CPU cores
- 4 GB RAM
- 20 GB or more disk space
- A public IPv4 or IPv6 address
- A domain or subdomain
The official Invidious documentation lists at least 20 GB of disk space and 2 GB of free RAM for Invidious plus Invidious Companion. Public instances need substantially more bandwidth and resources.
Compare Ubuntu VPS Plans
How to Deploy Invidious on Ubuntu VPS
To deploy Invidious on Ubuntu VPS, follow the steps provided below:
-
Update server
Check the server:
lsb_release -a free -h df -h nproc
Update Ubuntu:
sudo apt update sudo apt upgrade -y sudo reboot
Reconnect after the VPS restarts:
ssh your-user@YOUR_SERVER_IP
-
Configure the domain
Create a DNS record with your DNS provider.
For IPv4:
Type: A Name: invidious Value: YOUR_SERVER_IPV4 TTL: Auto
For IPv6, optionally add:
Type: AAAA Name: invidious Value: YOUR_SERVER_IPV6 TTL: Auto
The resulting hostname might be:
invidious.example.com
Verify that the domain resolves:
getent hosts invidious.example.com
You can also check the IPv4 address:
dig +short A invidious.example.com
Install
digwhen necessary:See Also: How to Install and Run Rootkit Hunter on AlmaLinux VPS
sudo apt install -y dnsutils
Do not request the TLS certificate until the domain resolves to the VPS.
-
Create an administrative user
Skip this section when the VPS already has a non-root administrative user.
Create a user:
sudo adduser deploy sudo usermod -aG sudo deploy
Copy the existing SSH keys:
sudo mkdir -p /home/deploy/.ssh sudo cp ~/.ssh/authorized_keys /home/deploy/.ssh/ sudo chown -R deploy:deploy /home/deploy/.ssh sudo chmod 700 /home/deploy/.ssh sudo chmod 600 /home/deploy/.ssh/authorized_keys
Open a second terminal and test the new account before closing the original session:
ssh deploy@YOUR_SERVER_IP
-
Secure SSH
Open the SSH server configuration:
sudo nano /etc/ssh/sshd_config
Recommended settings:
PermitRootLogin no PasswordAuthentication no PubkeyAuthentication yes
Only disable password authentication after confirming that SSH key login works.
Validate the configuration:
sudo sshd -t
Restart SSH:
sudo systemctl restart ssh
Keep the original SSH session open until a new login succeeds.
-
Configure the firewall
Install and configure UFW:
sudo apt install -y ufw sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow OpenSSH sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable
Check the result:
sudo ufw status verbose
Expected open ports:
22/tcp 80/tcp 443/tcp
Do not expose PostgreSQL port
5432, Invidious port3000, or Companion port8282publicly.Docker-published ports can bypass some UFW rules. This deployment avoids that problem for Invidious by binding port 3000 exclusively to
127.0.0.1. Docker specifically warns administrators to account for its interaction with UFW and firewall rules. -
Install required utilities
sudo apt install -y \ ca-certificates \ curl \ git \ openssl \ nano
-
Remove conflicting Docker packages
Remove Ubuntu-packaged or conflicting Docker components:
sudo apt remove -y \ docker.io \ docker-compose \ docker-compose-v2 \ docker-doc \ docker-buildx \ podman-docker \ containerd \ runc
It is harmless if Ubuntu reports that some packages are not installed. Docker recommends removing these conflicting packages before installing Docker Engine from Dockerās official repository.
-
Install Docker Engine
Add Dockerās signing key:
sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL \ https://download.docker.com/linux/ubuntu/gpg \ -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc
Add Dockerās APT repository:
sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<EOF Types: deb URIs: https://download.docker.com/linux/ubuntu Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") Components: stable Architectures: $(dpkg --print-architecture) Signed-By: /etc/apt/keyrings/docker.asc EOFInstall Docker Engine and Docker Compose V2:
See Also: How to Install Pleroma on Ubuntu VPS (5 Minute Quick-Start Guide)
sudo apt update sudo apt install -y \ docker-ce \ docker-ce-cli \ containerd.io \ docker-buildx-plugin \ docker-compose-plugin
These are the packages specified by Dockerās official Ubuntu installation procedure.
Enable Docker:
sudo systemctl enable --now docker
Verify the service:
sudo systemctl status docker --no-pager
Verify Docker Compose V2:
sudo docker compose version
The command must be:
docker compose
not the legacy:
docker-compose
Invidiousās production instructions explicitly require Docker Compose V2.
Run Dockerās test container:
sudo docker run --rm hello-world
-
Optionally allow the user to run Docker
Add your user to the
dockergroup:sudo usermod -aG docker "$USER"
Log out and reconnect:
exit
After reconnecting, test:
docker version docker compose version
Membership in the
dockergroup effectively grants root-level control over the server. Only add trusted administrative users.The rest of this guide shows Docker commands without
sudo. Addsudowhen you did not join the Docker group. -
Clone the Invidious repository
Use
/opt/invidiousas the deployment directory:sudo git clone https://github.com/iv-org/invidious.git /opt/invidious sudo chown -R "$USER":"$USER" /opt/invidious cd /opt/invidious
The repository is currently required because PostgreSQL initialization uses files from
config/sqlanddocker/init-invidious-db.sh.Confirm the files exist:
ls config/sql ls docker/init-invidious-db.sh
-
Generate secure secrets
Generate three independent secrets:
INVIDIOUS_HMAC_KEY="$(openssl rand -hex 32)" COMPANION_KEY="$(openssl rand -hex 32)" POSTGRES_PASSWORD="$(openssl rand -base64 36 | tr -d '/+=' | head -c 40)" printf 'HMAC key: %s\n' "$INVIDIOUS_HMAC_KEY" printf 'Companion key: %s\n' "$COMPANION_KEY" printf 'Database password: %s\n' "$POSTGRES_PASSWORD"
Save the values in a password manager.
You need:
- An Invidious HMAC key
- A separate Invidious Companion key
- A PostgreSQL password
Do not reuse the HMAC key as the Companion key. The official installation documentation specifically recommends separate keys.
-
Create the production Compose configuration
Back up the repositoryās existing Compose file:
cd /opt/invidious cp docker-compose.yml docker-compose.yml.repository
Create the production file:
nano docker-compose.yml
Paste the following configuration.
Replace all values marked
CHANGE_ME.services: invidious: image: quay.io/invidious/invidious:latest restart: unless-stopped ports: - "127.0.0.1:3000:3000" environment: INVIDIOUS_CONFIG: | db: dbname: invidious user: invidious password: "CHANGE_ME_DATABASE_PASSWORD" host: invidious-db port: 5432 check_tables: true domain: "invidious.example.com" external_port: 443 https_only: true registration_enabled: false statistics_enabled: false invidious_companion: - private_url: "http://companion:8282/companion" invidious_companion_key: "CHANGE_ME_COMPANION_KEY" hmac_key: "CHANGE_ME_HMAC_KEY" depends_on: invidious-db: condition: service_healthy companion: condition: service_started healthcheck: test: - CMD-SHELL - wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/stats || exit 1 interval: 30s timeout: 5s retries: 3 start_period: 30s logging: options: max-size: "100m" max-file: "4" companion: image: quay.io/invidious/invidious-companion:latest restart: unless-stopped environment: SERVER_SECRET_KEY: "CHANGE_ME_COMPANION_KEY" volumes: - companioncache:/var/tmp/youtubei.js:rw cap_drop: - ALL read_only: true security_opt: - no-new-privileges:true logging: options: max-size: "100m" max-file: "4" invidious-db: image: docker.io/library/postgres:14 restart: unless-stopped environment: POSTGRES_DB: invidious POSTGRES_USER: invidious POSTGRES_PASSWORD: "CHANGE_ME_DATABASE_PASSWORD" volumes: - postgresdata:/var/lib/postgresql/data - ./config/sql:/config/sql:ro - ./docker/init-invidious-db.sh:/docker-entrypoint-initdb.d/init-invidious-db.sh:ro healthcheck: test: - CMD-SHELL - pg_isready -U invidious -d invidious interval: 10s timeout: 5s retries: 10 logging: options: max-size: "100m" max-file: "4" volumes: postgresdata: companioncache:The current production architecture includes Invidious, PostgreSQL, and Invidious Companion. Companion replaces the older signature-helper and trusted-session-generator approach.
See Also: How to Install and Run Zulip on Debian VPS
Important replacements
Replace:
invidious.example.com
with your real hostname.
Replace both occurrences of:
CHANGE_ME_COMPANION_KEY
with exactly the same Companion key.
Replace both occurrences of:
CHANGE_ME_DATABASE_PASSWORD
with exactly the same PostgreSQL password.
Replace:
CHANGE_ME_HMAC_KEY
with the HMAC key.
The password and keys must remain quoted because generated values can contain characters that YAML may otherwise interpret.
-
Validate the Compose file
Run:
cd /opt/invidious docker compose config
This renders and validates the Compose configuration.
Carefully inspect the output for:
- YAML indentation errors
- Missing environment values
- Unexpected empty strings
- Duplicate fields
- Invalid service dependencies
To avoid displaying secrets on screen, use:
docker compose config --quiet
A successful validation produces no output and exits with status zero:
echo $?
Expected:
0
-
Pull the container images
docker compose pull
The Invidious production image is distributed through Quay, while the database image comes from the official PostgreSQL image repository.
Review downloaded images:
docker images
-
Start Invidious
docker compose up -d
Check container status:
docker compose ps
You should see three services:
invidious companion invidious-db
The database may take a short period to initialize on the first start.
Follow all service logs:
docker compose logs -f
Stop following logs with
Ctrl+C.See Also: š Guide to Using phpMyAdmin in cPanel
Inspect individual services:
docker compose logs --tail=100 invidious docker compose logs --tail=100 companion docker compose logs --tail=100 invidious-db
-
Test Invidious locally
Because Invidious is bound to the loopback address, it should be available from the VPS itself but not directly over the public internet.
Test the API:
curl -fsS http://127.0.0.1:3000/api/v1/stats
Test the homepage:
curl -I http://127.0.0.1:3000/
Expected result:
HTTP/1.1 200 OK
or another successful HTTP response.
Confirm the port is loopback-only:
sudo ss -lntp | grep ':3000'
Expected binding:
127.0.0.1:3000
Do not change this to
0.0.0.0:3000for a normal reverse-proxy deployment. The official production example also binds Invidious to127.0.0.1:3000. -
Install NGINX
sudo apt install -y nginx sudo systemctl enable --now nginx
Check its status:
sudo nginx -t sudo systemctl status nginx --no-pager
Open the server IP in a browser:
http://YOUR_SERVER_IP
You should see the NGINX default page.
-
Create the NGINX reverse proxy
Create a server configuration:
sudo nano /etc/nginx/sites-available/invidious
Paste:
server { listen 80; listen [::]:80; server_name invidious.example.com; location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Connection ""; proxy_buffering off; proxy_read_timeout 300s; proxy_send_timeout 300s; } }Replace:
invidious.example.com
with your real domain.
Invidiousās NGINX documentation proxies requests to
127.0.0.1:3000and includesX-Forwarded-Host.Enable the site:
sudo ln -s /etc/nginx/sites-available/invidious \ /etc/nginx/sites-enabled/invidious
Optionally remove the default site:
sudo rm -f /etc/nginx/sites-enabled/default
Validate NGINX:
sudo nginx -t
Apply the configuration:
sudo systemctl reload nginx
Test HTTP:
curl -I http://invidious.example.com
At this stage, the instance should load over unencrypted HTTP.
-
Install Certbot
Install Certbot and its NGINX plugin:
sudo apt update sudo apt install -y certbot python3-certbot-nginx
Request a certificate:
sudo certbot --nginx -d invidious.example.com
Certbot will ask for:
- An email address
- Agreement to the terms
- Whether to redirect HTTP to HTTPS
Select the HTTPS redirect.
Test the site:
curl -I https://invidious.example.com
Open it in a browser:
https://invidious.example.com
-
Verify certificate renewal
Check the Certbot timer:
See Also: Comprehensive List of Top 10 Uses for VPS Servers
sudo systemctl status certbot.timer --no-pager
Test renewal without changing the certificate:
sudo certbot renew --dry-run
List installed certificates:
sudo certbot certificates
-
Verify the full deployment
Check containers
cd /opt/invidious docker compose ps
Check health information
docker inspect \ --format='{{json .State.Health}}' \ "$(docker compose ps -q invidious)"Test the public API
curl -fsS \ https://invidious.example.com/api/v1/stats
Test NGINX
sudo nginx -t systemctl is-active nginx
Check exposed ports
sudo ss -lntp
The expected public listening ports are:
22 80 443
Port
3000should only listen on127.0.0.1.Test basic functionality
In the browser:
- Open the homepage.
- Search for a video.
- Open a video page.
- Test playback.
- Open a channel.
- Test an RSS feed.
- Confirm HTTPS remains active throughout.
Search results working does not necessarily mean video playback works. Playback depends on Invidious Companion being configured and healthy. The official documentation notes that playback does not work correctly without Companion.
-
Disable public registration
This guide sets:
registration_enabled: false
That is a sensible default for a private instance.
After changing any
INVIDIOUS_CONFIGvalue, recreate the Invidious container:cd /opt/invidious docker compose up -d --force-recreate invidious
Check logs:
docker compose logs --tail=100 invidious
-
Updating Invidious
Enter the deployment directory:
cd /opt/invidious
Back up the database first:
mkdir -p backups docker compose exec -T invidious-db \ pg_dump -U invidious -d invidious \ > "backups/invidious-$(date +%F-%H%M%S).sql"
Pull repository updates:
git status git pull --ff-only
If Git refuses because
docker-compose.ymlwas modified, save your production file first:cp docker-compose.yml /tmp/invidious-compose.yml git restore docker-compose.yml git pull --ff-only cp /tmp/invidious-compose.yml docker-compose.yml
Review upstream changes before continuing:
git log --oneline -10 git diff HEAD~1 -- config/sql docker/init-invidious-db.sh
Pull current images:
docker compose pull
Recreate services:
docker compose up -d
Remove unused images:
docker image prune -f
Verify:
docker compose ps docker compose logs --tail=100 curl -fsS https://invidious.example.com/api/v1/stats
-
Backing up the database
Create a backup directory:
sudo mkdir -p /var/backups/invidious sudo chown "$USER":"$USER" /var/backups/invidious sudo chmod 700 /var/backups/invidious
Create a compressed database backup:
cd /opt/invidious docker compose exec -T invidious-db \ pg_dump -U invidious -d invidious \ | gzip \ > "/var/backups/invidious/invidious-$(date +%F-%H%M%S).sql.gz"
Confirm the backup:
See Also: Install Modoboa Email Server on Debian VPS (1-Stop Complete Guide)
ls -lh /var/backups/invidious gzip -t /var/backups/invidious/invidious-*.sql.gz
A backup stored only on the same VPS is not sufficient. Copy backups to another server or encrypted object-storage location.
-
Automate database backups
Create a backup script:
sudo nano /usr/local/sbin/backup-invidious
Paste:
#!/usr/bin/env bash set -Eeuo pipefail DEPLOYMENT_DIR="/opt/invidious" BACKUP_DIR="/var/backups/invidious" RETENTION_DAYS="14" mkdir -p "$BACKUP_DIR" chmod 700 "$BACKUP_DIR" cd "$DEPLOYMENT_DIR" filename="$BACKUP_DIR/invidious-$(date +%F-%H%M%S).sql.gz" docker compose exec -T invidious-db \ pg_dump -U invidious -d invidious \ | gzip > "$filename" gzip -t "$filename" find "$BACKUP_DIR" \ -type f \ -name 'invidious-*.sql.gz' \ -mtime +"$RETENTION_DAYS" \ -delete
Make it executable:
sudo chmod 750 /usr/local/sbin/backup-invidious
Test it:
sudo /usr/local/sbin/backup-invidious sudo ls -lh /var/backups/invidious
Create a systemd service:
sudo nano /etc/systemd/system/invidious-backup.service
Paste:
[Unit] Description=Back up the Invidious PostgreSQL database Requires=docker.service After=docker.service [Service] Type=oneshot ExecStart=/usr/local/sbin/backup-invidious
Create a timer:
sudo nano /etc/systemd/system/invidious-backup.timer
Paste:
[Unit] Description=Run the Invidious database backup daily [Timer] OnCalendar=*-*-* 03:30:00 Persistent=true RandomizedDelaySec=10m [Install] WantedBy=timers.target
Enable it:
sudo systemctl daemon-reload sudo systemctl enable --now invidious-backup.timer
Verify:
systemctl list-timers --all | grep invidious
-
Restore the database
Stop Invidious while leaving PostgreSQL running:
cd /opt/invidious docker compose stop invidious companion docker compose up -d invidious-db
Drop and recreate the database:
docker compose exec -T invidious-db \ psql -U invidious -d postgres \ -c "DROP DATABASE IF EXISTS invidious;" docker compose exec -T invidious-db \ psql -U invidious -d postgres \ -c "CREATE DATABASE invidious OWNER invidious;"
Restore a compressed backup:
gunzip -c /var/backups/invidious/invidious-BACKUP.sql.gz \ | docker compose exec -T invidious-db \ psql -U invidious -d invidiousRestart everything:
docker compose up -d docker compose ps
Check logs:
docker compose logs --tail=100
Always test the restore process before relying on the backup strategy.
-
Useful operational commands
View status
cd /opt/invidious docker compose ps
View all logs
docker compose logs -f
View recent Invidious logs
docker compose logs --tail=200 invidious
Restart Invidious
docker compose restart invidious
Restart all services
docker compose restart
Stop the deployment
docker compose down
This does not delete the named database volume.
Start the deployment
docker compose up -d
Show resource use
docker stats
Show disk use
docker system df
Validate configuration
docker compose config --quiet
-
Troubleshooting
Invidious container keeps restarting
Check:
See Also: š How to Deploy Supabase on AlmaLinux VPS (Production-Ready Guide)
docker compose ps docker compose logs --tail=200 invidious
Common causes include:
- Invalid YAML under
INVIDIOUS_CONFIG - Missing HMAC key
- Companion key mismatch
- Database password mismatch
- Database still initializing
- Incorrect indentation
- Insufficient memory
Validate Compose syntax:
docker compose config --quiet
Database authentication fails
Look for errors such as:
password authentication failed
The password must match in both locations:
INVIDIOUS_CONFIG: db: password: "..."and:
invidious-db: environment: POSTGRES_PASSWORD: "..."Changing
POSTGRES_PASSWORDafter PostgreSQL has initialized does not automatically change the password stored inside an existing database volume.For a new installation with no data worth preserving, remove and rebuild the database volume:
docker compose down docker volume ls | grep postgresdata docker volume rm invidious_postgresdata docker compose up -d
This permanently deletes the Invidious database.
Companion errors or videos do not play
Check Companion:
docker compose logs --tail=200 companion
Confirm that these values are identical:
invidious_companion_key: "YOUR_COMPANION_KEY"
and:
SERVER_SECRET_KEY: "YOUR_COMPANION_KEY"
Confirm the private URL is:
private_url: "http://companion:8282/companion"
Recreate both services:
docker compose up -d --force-recreate companion invidious
Invidious Companion is now part of the expected deployment and replaces older helper components.
NGINX returns 502 Bad Gateway
Check whether Invidious is running:
docker compose ps curl -I http://127.0.0.1:3000
Check logs:
docker compose logs --tail=100 invidious sudo tail -n 100 /var/log/nginx/error.log
Confirm NGINX uses:
proxy_pass http://127.0.0.1:3000;
Restart services:
docker compose restart invidious sudo systemctl restart nginx
Certificate issuance fails
Verify DNS:
dig +short A invidious.example.com dig +short AAAA invidious.example.com
Verify port 80:
sudo ufw status sudo ss -lntp | grep ':80'
Verify NGINX:
sudo nginx -t sudo systemctl status nginx --no-pager
An incorrect AAAA record can cause validation failures even when the A record is correct. Remove an unusable AAAA record or configure IPv6 correctly.
Out-of-memory errors
Check memory:
free -h sudo dmesg -T | grep -i -E 'out of memory|oom|killed process'
Add a 4 GB swap file when the VPS has limited RAM:
sudo fallocate -l 4G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile
Persist it:
echo '/swapfile none swap sw 0 0' \ | sudo tee -a /etc/fstab
Check:
swapon --show free -h
Swap can reduce abrupt out-of-memory failures but is not a substitute for sufficient RAM.
See Also: šØ Email Server Administration: Ensuring Email Deliverability to Gmail and Yahoo
Disk usage grows unexpectedly
Check Docker usage:
docker system df
Check logs:
sudo du -sh /var/lib/docker/containers/* 2>/dev/null \ | sort -h \ | tail
Remove unused images:
docker image prune -f
Do not run this unless you understand what it removes:
docker system prune
Never add
--volumescasually, because volumes may contain the PostgreSQL database. - Invalid YAML under
-
Security checklist
Before considering the deployment complete, confirm:
- SSH root login is disabled.
- SSH password login is disabled after key authentication is tested.
- UFW only exposes SSH, HTTP, and HTTPS.
- Invidious listens on
127.0.0.1:3000. - PostgreSQL has no public port mapping.
- Companion has no public port mapping.
- HTTPS works.
- HTTP redirects to HTTPS.
- Registration is disabled unless intentionally enabled.
- HMAC, Companion, and database secrets are different.
- Compose and backup files are readable only by administrators.
- Database backups are created and tested.
- Off-server backups exist.
- Ubuntu and Docker receive regular security updates.
- Container logs have size limits.
- The instance is monitored for disk, memory, and container health.
-
Final architecture
The deployed request flow is:
Browser | | HTTPS :443 v NGINX | | HTTP over loopback v Invidious :3000 | \ | \ Internal Docker network v v PostgreSQL Invidious Companion
Only NGINX is publicly accessible. Invidious, PostgreSQL, and Companion remain behind the VPS and Docker networking layers.
The official production Compose method is designed for Invidious to sit behind a reverse proxy rather than exposing its application port directly.
Conclusion
You now know how to deploy Invidious on Ubuntu VPS.










