Install Luvio with Docker
This guide runs the Luvio server in containers on one Linux machine with Docker. Two layouts are available:
| Layout | What runs | When to choose it |
|---|---|---|
| Docker Compose (recommended) | Three containers: Luvio, its template transfer worker and PostgreSQL 17 | Most installations. The database is a standard PostgreSQL container you can back up with the usual tools. |
| Single container | One container with Luvio and its own PostgreSQL | A test, a demonstration, or when one container is all you can run. |
At the end of this guide, the Luvio web console answers on http://<docker host>:3080 and you
go on with First setup.
To run Luvio on several Docker hosts for high availability, see Docker application nodes.
Before you begin
Section titled “Before you begin”You need:
- A Linux machine with Docker Engine 24 or later and the Compose plugin 2.24 or later.
Check them with
docker versionanddocker compose version. This guide was validated on Ubuntu 24.04 LTS with Docker 28 and Compose 2. - At least 2 vCPU, 4 GB of RAM and 40 GB of disk for Docker’s volumes. Plan more disk if you use Universal Templates (see Plan your installation).
- A fixed IP address or a DNS name for the machine: the virtual machines connect back to it. You give it to Luvio at step 2.
- The time of the machine synchronized with NTP.
- TCP port 3080 free on the machine.
1. Get the images
Section titled “1. Get the images”Without Internet access (offline bundle). Download luvio-docker-<version>-offline.tar.gz
from <DOWNLOAD_URL> and copy it to the Docker machine. It holds the images of Luvio and of
PostgreSQL 17 for x86-64 processors, and the docker-compose.yml file. Unpack it in the directory
that will hold the installation, then load the images:
sudo mkdir -p /opt/luvio && cd /opt/luviosudo tar --no-same-owner -xzf ~/luvio-docker-<version>-offline.tar.gzsudo docker load -i luvio-docker-<version>.tardocker load prints Loaded image: luvio:<version> and Loaded image: postgres:17.
With access to the registry. Download docker-compose.yml from <DOWNLOAD_URL> into
/opt/luvio, then replace luvio:<version> with <REGISTRY>/luvio:<version> in its two image:
lines. docker compose downloads the images at the next step.
2. Create the .env file
Section titled “2. Create the .env file”In /opt/luvio, create the .env file. Docker Compose reads it by itself. Replace
luvio.example.com with the DNS name or the IP address of the Docker machine, as the virtual
machines reach it, without http:// and without a port:
cd /opt/luviosudo sh -c 'cat > .env <<EOFLUVIO_PUBLIC_HOSTNAME=luvio.example.comLUVIO_DB_PASSWORD=$(openssl rand -hex 24)LUVIO_SESSION_SECRET=$(openssl rand -hex 48)LUVIO_CONFIG_ENCRYPTION_KEY=$(openssl rand -hex 48)EOF'sudo chmod 600 .envLUVIO_PUBLIC_HOSTNAME is the Server URL that Luvio gives to the agents in the virtual
machines. Inside its container, Luvio only sees the container’s own address (for example
172.18.0.3), which the virtual machines cannot reach.
LUVIO_CONFIG_ENCRYPTION_KEY encrypts the hypervisor passwords and the other secrets Luvio stores
in its database. Without it, a backup of the database cannot be restored, and it must never
change. Copy .env to your password safe or your backup system now.
3. Start Luvio
Section titled “3. Start Luvio”cd /opt/luviosudo docker compose up -dThe three containers restart by themselves with the machine.
4. Check that Luvio runs
Section titled “4. Check that Luvio runs”-
List the containers:
Terminal window sudo docker compose psThe three containers must be
Up. After a minute,dbandluvioalso show(healthy). -
Check that the web server answers:
Terminal window curl -fsS http://localhost:3080/readyzThe answer starts with
{"status":"ready": Luvio has built its database at its first start. If it is{"status":"not_ready"..., wait a minute, then read the logs (Troubleshooting). -
From your workstation, open
http://<docker host>:3080. The Welcome to Luvio page appears.
Docker opens port 3080 itself in the machine’s packet filter: you do not open it in firewalld or ufw. A network firewall between the users and the machine must still allow it.
5. Go on with the first setup
Section titled “5. Go on with the first setup”Open http://<docker host>:3080 and follow First setup. Luvio has built its
database already: the wizard opens on the hypervisor. In Administration > Server & storage,
the Server URL shows http://<LUVIO_PUBLIC_HOSTNAME>:3080 with From environment.
To skip the wizard, give the hypervisor and the administrator in a configuration file.
Settings
Section titled “Settings”The settings are in the environment: sections of docker-compose.yml, and the address in .env.
After a change, apply it:
sudo docker compose up -d| Setting | Default | Use |
|---|---|---|
LUVIO_PUBLIC_HOSTNAME (in .env) |
empty | DNS name or IP address of the Docker machine, the Server URL of the agents. Empty: set the Server URL in Administration > Server & storage instead |
SERVER_PROTOCOL |
http |
http, https or both |
HTTP_PORT |
3080 |
Port of the HTTP listener |
HTTPS_PORT |
3443 |
Port of the HTTPS listener |
TRUSTED_PROXIES |
empty | Addresses of the reverse proxies in front of Luvio: see Behind a reverse proxy |
LUVIO_CONFIG_FILE (in .env) |
empty | The configuration file: /etc/luvio/config/luvio-config.yaml |
LUVIO_ADMIN_PASSWORD (in .env) |
empty | Password of the first admin account, created only when there is none |
LUVIO_NO_HYPERVISOR (in .env) |
empty | true (or 1, yes; false, 0, no or empty: off, any other value is an error): physical machines only, no hypervisor (hypervisors: []). With LUVIO_ADMIN_PASSWORD, the installation is complete without the setup wizard |
LUVIO_TLS_CERT_FILE, LUVIO_TLS_KEY_FILE (in .env) |
empty | Your certificate for HTTPS: see Switch to HTTPS. You can also import one in Administration > Server & storage > HTTPS certificate; these files win over it |
The port Luvio listens on must be published under the same number ("3080:3080"): Luvio tells
the agents to connect to it. That is why the protocol and the ports are set here, with the published
ports, and not in the web console: Administration > Server & storage shows them with From
environment.
Switch to HTTPS
Section titled “Switch to HTTPS”In the luvio service, set SERVER_PROTOCOL: both, add HTTPS_PORT: "3443", and publish the
port:
ports: - "3080:3080" - "3443:3443"Then run sudo docker compose up -d. Keep HTTP on as well (both): the health check of the
container uses it. The agents use HTTPS once it is on.
With both, port 3080 stays open for the agents, the desktop clients, the identity providers and
the health check, and the web console moves to HTTPS: a browser that opens
http://<docker host>:3080 is sent to https://<docker host>:3443, and the sign-in and the
console’s API refuse HTTP. A session opened over HTTPS has its own cookie, which browsers never
send over HTTP. HTTP requests from inside the container, and those of a trusted reverse proxy
(below), still reach the console over HTTP.
HTTPS answers tell the browser to use HTTPS for this host name for one day (HSTS). Browsers ignore
this for an IP address and for a certificate they do not trust. When they apply it, they open every
http:// address of that host name over HTTPS, port 3080 included, which fails: type https://
addresses in the browser. Single sign-on to the web console comes back to the HTTPS address: the
identity provider must accept https://<docker host>:3443/api/auth/oidc/callback (OpenID Connect)
or the SAML metadata of https://<docker host>:3443/api/auth/saml/metadata, and a Redirect URI
set in the OpenID Connect settings must be that HTTPS address.
By default, Luvio serves HTTPS with a self-signed certificate that it creates and renews
itself: browsers show a warning, and the desktop clients ask the user to trust it the first time.
To serve your own certificate, valid for LUVIO_PUBLIC_HOSTNAME:
-
Put the certificate (PEM, with its chain) and its key in
/opt/luvio/tls, readable by the user of the container:Terminal window cd /opt/luviosudo mkdir -p tlssudo cp luvio.crt tls/tls.crtsudo cp luvio.key tls/tls.keysudo chown -R 1000:1000 tlssudo chmod 600 tls/tls.key -
In
docker-compose.yml, remove the#in front of the./tlsline of theluvioservice:- ./tls:/etc/luvio/tls:ro,z. -
Add to
.env:Terminal window LUVIO_TLS_CERT_FILE=/etc/luvio/tls/tls.crtLUVIO_TLS_KEY_FILE=/etc/luvio/tls/tls.key -
Run
sudo docker compose up -d.
Luvio reads the files again when they change (a renewal), without a restart.
Behind a reverse proxy
Section titled “Behind a reverse proxy”A reverse proxy can serve the web console with your certificate. In the luvio service, set
TRUSTED_PROXIES to the address the proxy connects from, as Luvio sees it: IP addresses or
networks (CIDR), separated by commas. For a proxy in another container of the same Compose
project, it is the subnet of the project’s network (sudo docker network inspect <project>_default); for a proxy on the Docker machine itself, the gateway of that network.
TRUSTED_PROXIES: "172.18.0.0/16"Luvio then reads the X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host headers that
the proxy adds: it sees the users’ own addresses (the sign-in limits count per address), and
knows when the proxy serves HTTPS. A session opened through an HTTPS proxy then gets a Secure
cookie, and the answers carry HSTS. Luvio ignores these headers from any other address. With
both, a browser that reaches port 3080 directly is still sent to HTTPS; requests of a trusted
proxy are served, over HTTP or HTTPS as the proxy reports: make the proxy send its own HTTP
visitors to HTTPS.
Without the setting, every user has the address of the proxy, and a session opened through an
HTTPS proxy gets a cookie without Secure. Trust nothing but the proxy: a client that connects
from a trusted address can choose the address and the protocol that Luvio sees.
Use an external PostgreSQL
Section titled “Use an external PostgreSQL”To use a PostgreSQL server of your own (12 or later): remove the db service and its
depends_on entries from docker-compose.yml, then set PGHOST, PGPORT, PGDATABASE,
PGUSER (in x-luvio-environment at the top of the file) and LUVIO_DB_PASSWORD (in .env) to
that server. The account needs the CREATEDB right, unless the database exists already and
belongs to it: Luvio builds the database at its first start. Connect it to PostgreSQL directly,
never through PgBouncer in transaction mode.
Upgrade
Section titled “Upgrade”-
Take a backup.
-
Load the new images (
docker load -i luvio-docker-<new version>.tar), or skip this step with a registry. -
In
docker-compose.yml, change the tag of the twoimage:lines to the new version. -
Apply:
Terminal window sudo docker compose up -d
Keep .env and the volumes: they hold the keys and the data.
Then upgrade the agents: the Luvio server and its agents must be the same release, and an agent older than the server refuses its settings and its commands (see Upgrade Luvio and every agent together in Templates, agent and pools). With application nodes, upgrade every node before the agents.
Universal Templates on Proxmox VE: from a version that did not check the SSH host keys of the nodes, copies stop until you trust the key of each node, in the Universal Templates panel of the hypervisor (see Universal Templates).
Back up and restore
Section titled “Back up and restore”A backup is three things: .env, the database, and the luvio-data volume (the identity of the
server, its HTTPS certificate, the uploaded files). Docker Compose names the volumes after the
directory, here luvio_luvio-data and luvio_luvio-db.
cd /opt/luviosudo docker compose exec -T db pg_dump -U luvio -Fc luvio > ~/luvio-db-$(date +%F).dumpsudo docker run --rm -v luvio_luvio-data:/var/lib/luvio --entrypoint tar luvio:<version> \ -czf - -C /var/lib/luvio . > ~/luvio-data-$(date +%F).tar.gzsudo cat .env > ~/luvio-env-$(date +%F)Keep the three files outside the machine: .env holds the keys.
To restore them on a new machine: install as above with the saved .env (not a new one), but
instead of docker compose up -d, create the containers, start only the database, restore it, then
start everything:
cd /opt/luviosudo docker compose createsudo docker compose up -d dbsudo docker compose exec -T db pg_restore -U luvio -d luvio --clean --if-exists < luvio-db-<date>.dumpsudo docker run --rm -i -v luvio_luvio-data:/var/lib/luvio --entrypoint tar luvio:<version> \ -xzf - -C /var/lib/luvio < luvio-data-<date>.tar.gzsudo docker compose up -dA restore brings Luvio back to the time of the backup: desktops created after it are unknown to Luvio; delete them from the hypervisor.
Uninstall
Section titled “Uninstall”cd /opt/luviosudo docker compose downThis removes the containers and keeps the volumes: docker compose up -d brings Luvio back with
its data. To delete the data too, after a backup, run sudo docker compose down -v.
Single container
Section titled “Single container”The single-container image runs Luvio, its template transfer worker and its own PostgreSQL 17 in one container. It generates its secrets at the first start and keeps them in its volume.
-
Load the image from
luvio-docker-<version>-standalone-offline.tar.gz:Terminal window tar -xzf luvio-docker-<version>-standalone-offline.tar.gzsudo docker load -i luvio-docker-<version>-standalone.tarWith a registry, the image is
<REGISTRY>/luvio:<version>-standalone. -
Start it:
Terminal window sudo docker run -d --name luvio --restart unless-stopped \-p 3080:3080 \-e PUBLIC_HOSTNAME=luvio.example.com \-v luvio-data:/var/lib/luvio \luvio:<version>-standaloneReplace
luvio.example.comwith the DNS name or the IP address of the Docker machine, as the virtual machines reach it (see step 2). -
Check it with
curl -fsS http://localhost:3080/readyz({"status":"ready"...), then follow First setup.
The secrets are in /var/lib/luvio/luvio.env inside the volume. Save them:
sudo docker exec luvio cat /var/lib/luvio/luvio.envTo upgrade, load the new image, then remove the container and start it again with the new tag, the
same volume and the same PUBLIC_HOSTNAME. To back up, stop the container and save the volume:
sudo docker stop luviosudo docker run --rm -v luvio-data:/var/lib/luvio --entrypoint tar luvio:<version>-standalone \ -czf - -C /var/lib/luvio . > ~/luvio-standalone-$(date +%F).tar.gzsudo docker start luvioTroubleshooting
Section titled “Troubleshooting”Read the logs first:
sudo docker compose logs -f luvio # web console, APIsudo docker compose logs -f luvio-transfer # template transferssudo docker compose logs -f db # PostgreSQL| Symptom | Cause and fix |
|---|---|
required variable LUVIO_DB_PASSWORD is missing a value |
The .env file is missing or is not in the directory of docker-compose.yml. Create it (step 2). |
The desktops stay Not connected; the Server URL is http://172.x.x.x:3080 with the warning The virtual machines probably cannot reach this address |
LUVIO_PUBLIC_HOSTNAME is missing from .env, or is not a name or an address (the logs of luvio then say PUBLIC_HOSTNAME="…" is not a host name or an IP address). Fix it and run sudo docker compose up -d, or set the Server URL in Administration > Server & storage. Luvio sends the new address to the agents that were given the container’s address. A desktop that stays Not connected: select it in Manage Machines and use Resend Agent Config. |
Bind for 0.0.0.0:3080 failed: port is already allocated |
Another program or container uses port 3080. Stop it, or change HTTP_PORT and the published port together. |
luvio is unhealthy after a switch to HTTPS only |
The health check uses HTTP: set SERVER_PROTOCOL: both. |
password authentication failed for user "luvio" in the logs of luvio |
LUVIO_DB_PASSWORD changed after the database was created: PostgreSQL keeps the first password. Put back the original .env. |
Unsupported state or unable to authenticate data |
LUVIO_CONFIG_ENCRYPTION_KEY is not the key the database was encrypted with. Put back the original .env. |