Summary
In 2020 I wrote up a recipe for running Vivado in an LXC container, and dismissed Docker as "more of a hassle." I have since moved nearly everything to Docker. This page describes what changed and how the Docker version works.
The short version: don't bake Vivado into a Docker image. Install it on the host as usual, and bind-mount the installation into a small container that captures an officially-supported OS. Keep the Dockerfile in the project repository, build the image locally, and don't bother with hosted Docker infrastructure.
The Problem (Unchanged)
Each Vivado release is supported on a short list of Linux distributions and releases. Debian isn't on it, and I run Debian all day. Vivado usually works fine on Debian (if you have enough free disk space), but "usually" is not good enough for a build server, and every so often a release is problematic enough to need a real Ubuntu underneath it.
The LXC page has the full rant. Nothing there is wrong, and the LXC recipe still works.
What Changed
The LXC recipe treats the container as a second PC: a long-lived guest with its own users, its own sshd, and X11 forwarding back to the host. That's fine for one person on one workstation, but the guest becomes one more machine to maintain, and there is nothing in it that lives alongside the project's source code.
My objection to Docker at the time was that Vivado is enormous, and scripting an unattended Xilinx installation is not easy. Both of these things are still true but irrelevant:
- Bind-mount the Vivado installation. Install Vivado on the host (or on a volume that's persistent outside the Docker environment) by running the installer the normal way. The container sees it at the same path as the host, so settings64.sh works unchanged. This avoids having to script the installation and keeps the Docker image simple and small.
- Keep the Dockerfile in the repository. The image captures only an approved OS release and a handful of packages. It builds in a minute or two, so there is no reason to push it to a registry. Because the Dockerfile is version-controlled alongside the design, each project pins the Vivado release and the OS it expects, and old trees keep working.
- Let `make` hop into the container by itself. A short Makefile boilerplate detects when it's running outside Docker and re-executes itself inside. Nobody needs to remember a docker run incantation, and the same Makefile works on a laptop, a build server, or inside a git hook.
This page is about batch use: bitstream builds, simulations, and regression tests. For the GUI, I run Vivado directly on the Debian host. When a release is problematic there, Docker with bind-mounts to /opt means I don't need to re-install anything.
The Recipe
There are three pieces: a Dockerfile, a Makefile boilerplate, and the bind-mounts. The examples below are from pyxsi, which is public and small enough to read in one sitting.
The Dockerfile
This captures an Ubuntu LTS release that shows up in Vivado's release notes, plus whatever the project needs (here: a C++ toolchain, pybind11, and pytest).
# This Dockerfile captures a Ubuntu 24.04 suitable for Vivado. This is one of
# the LTS releases that shows up in the Vivado release notes.
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND noninteractive
ENV LD_LIBRARY_PATH=.:${XILINX_VIVADO}/lib/lnx64.o:${XILINX_VIVADO}/lib/lnx64.o/Ubuntu
WORKDIR /root
RUN apt-get update -qq \
&& apt-get install -y --no-install-recommends \
ca-certificates \
make build-essential g++ \
python3 python3-dev \
python3-pytest python3-pytest-forked \
libfmt-dev pybind11-dev python3-pybind11 \
locales wget valgrind \
libx11-6 \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/*
# Install libtinfo5, which recent Vivado still seems to rely on
RUN wget http://security.ubuntu.com/ubuntu/pool/universe/n/ncurses/libtinfo5_6.3-2ubuntu0.1_amd64.deb && \
apt install ./libtinfo5_6.3-2ubuntu0.1_amd64.deb
# make /bin/sh symlink to bash instead of dash
RUN dpkg-divert --remove --no-rename /bin/sh
RUN ln -sf bash /bin/sh
RUN dpkg-divert --add --local --no-rename /bin/sh
# Generate en_US.UTF-8 (required for Vivado)
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
There's no Vivado here, except:
- libtinfo5 is no longer packaged in Ubuntu 24.04, but Vivado still wants it. (The exact .deb version churns; expect to update this line occasionally.)
- Vivado's scripts assume /bin/sh is bash, not dash.
- Vivado wants an en_US.UTF-8 locale, which a minimal image doesn't have.
Makefile Boilerplate
The project Makefile starts like this:
ifeq ($(XILINX_VIVADO),)
$(error Please source a Vivado settings.sh script before running this!)
endif
# See if we're inside docker. If not, wrap ourselves in docker and try again.
ifeq ($(wildcard /.dockerenv),)
include Makefile.docker-boilerplate
else
# For the remainder of this Makefile, we're running within Docker and can
# focus on the actual build.
[... ordinary Makefile rules go here ...]
endif
Outside the container, /.dockerenv doesn't exist and the boilerplate takes over. It builds the image if necessary, then re-runs make with the same goals inside the container:
.PHONY: $(MAKECMDGOALS) dockerenv
# Learn where the git repository lives, and what subdirectory we're working in.
REPO_ROOT := $(shell git rev-parse --show-toplevel)
REPO_SUBDIR := $(shell git rev-parse --show-prefix)
DOCKERSTAMP := $(REPO_ROOT)/.dockerenv-$(shell hostname)
DOCKERNAME := pyxsi-docker
# Unfortunately there are two conventions for Vivado's install path:
#
# 2025.1 and newer: /opt/xilinx/2025.1/Vivado
# older : /opt/xilinx/Vivado/2023.2
#
# To be compatible with both, we share the common root
XILINX_ROOT=$(realpath $(XILINX_VIVADO)/../..)
# Use Docker to build per the rest of this Makefile.
$(MAKECMDGOALS) default: $(DOCKERSTAMP)
@echo Moving inside Docker...
@docker run \
-u $(shell id -u):$(shell id -g) \
-v $(REPO_ROOT):/root \
-v $(XILINX_ROOT):/$(XILINX_ROOT) \
-e XILINX_VIVADO -e XILINX_HLS -e XILINX_VITIS \
$(DOCKERNAME) \
make -C /root/$(REPO_SUBDIR) $(MAKECMDGOALS)
# We need to re-build the Docker image anytime Dockerfile changes, or if we
# haven't done this before.
$(DOCKERSTAMP) dockerenv: $(REPO_ROOT)/Dockerfile
docker build -t $(DOCKERNAME) $(REPO_ROOT)
touch $(DOCKERSTAMP)
Notes:
- -u $(id -u):$(id -g) runs the build as you, so everything it writes into the bind-mounted tree is owned by you and not by root. There is no user inside the image at all.
- -v $(XILINX_ROOT):/$(XILINX_ROOT) is the bind-mount. The install path is identical inside and outside, so XILINX_VIVADO (and anything derived from it) passes straight through with -e.
- The stamp file is per-hostname, because the repository may be checked out on a shared filesystem and visible to several machines. It depends on the Dockerfile, so editing the Dockerfile rebuilds the image on the next make.
In larger projects, I bind-mount $HOME instead of just the repository, which lets a build reach sibling checkouts and the git-lfs cache. It also helps to pass the -j flag along (GNU make's jobserver doesn't cross the Docker boundary), and to allocate a pty (-it) only for the targets that actually need one, such as an interactive ipython3 session. A pty can't always be allocated, and a git hook has no terminal to give you.
Running It
$ . /opt/xilinx/2026.1/Vivado/settings64.sh
$ cd pyxsi
pyxsi$ make rtl
Moving inside Docker...
[snip]
Built XSI simulation shared library xsim.dir/widget/xsimk.so
pyxsi$ make test
Moving inside Docker...
[snip]
============================== 8 passed in 31.57s ==============================
That's it. There is nothing to attach to, nothing to ssh into, and nothing that outlives the build.
Questions and Answers
How do Vivado licenses work with Docker?
Use --network=host, and license the host as if you weren't using Docker at all. The container then sees the host's interfaces, MAC addresses, and hostname, and a node-locked license file for the host just works. (The --hostname and --mac-address options exist if you would rather give the container its own identity, but I haven't needed them.)
Why not bake Vivado into the image?
The image would be tens of gigabytes, take an hour to build, and need rebuilding for every point release. Installing the tools into a bind-mount that's persistent outside the Docker environment avoids having to script the installation (you just run the installer and point it at the mount), and it keeps your Docker images simple and small. A side benefit is that the host and every container share one installation, which matters when you have half a dozen Vivado releases on disk.
Why not push images to a registry?
The usual argument for a registry is reproducibility: every runner gets exactly the same tools at exactly the same version. Here, the tools are in the bind-mount, and the image is a few hundred megabytes of Ubuntu that builds from a version-controlled Dockerfile in a minute. The registry would be one more piece of infrastructure to manage, and it would pull part of the build configuration out of the tree. (We have a nearly pathological aversion to IT infrastructure that requires maintenance.)
Why Docker now, when LXC was fine?
LXC was fine, and still is. Docker won the populaity contents and its ergonomics are fine. It also composes with other tools (remote-build hooks, Yocto) in a way that a long-lived LXC guest never did.
Surely there's a Catch?
The kernel is still shared between host and container, so the caveat from the LXC page still applies: a kernel-level feature on the host can tickle a bug in Vivado that it wouldn't hit natively. In practice, I have not run into this.
The other catch is that a container is not a desktop. If what you want is Vivado's GUI on an OS it approves of, the LXC recipe (or a VM) is still the right tool.