CYBERTEC PostgreSQL Logo

Dev Container for pgrx PostgreSQL Extensions: Lessons Learned

12.2025
Category: 
Tags:  , ,

I like reproducible development. I also like short feedback loops. Combining both for pgrx was… educational. 🙂 In this post, I share the mistakes, the small pains, and the fixes I used to get a working VS Code dev container for a Rust project that builds PostgreSQL extensions with pgrx. If you’re writing extensions or using pgrx in a team, this will save you a few grey hairs.

TL;DR:

  • PostgreSQL refuses to run as root. Don’t run your devcontainer as root if cargo pgrx test must work.
  • Install user-specific Rust tools as the non-root user, remember that order matters.
  • Run cargo pgrx init after the container starts (use postCreateCommand) so the config persists.

A quick story

I was setting up a devcontainer for an etcd_fdw project that uses pgrx to produce a PostgreSQL extension. At first, it seemed straightforward: start from the official Rust image, install build deps, add cargo-pgrx, and be done.

But real life is noisy. I hit permission errors, failing tests, and an annoying config.toml not found. Have you run 'cargo pgrx init' yet?. After some digging, I discovered three core issues that you should watch out for.

The PostgreSQL root problem

PostgreSQL will not run as root by design. cargo pgrx test launches PostgreSQL instances for integration testing. If your devcontainer runs as root (or if you initialize pgrx as root and then switch user incorrectly), tests will fail.

First, I tried to set remoteUser: "root" in devcontainer.json and installing everything as root. The build failed with errors when tests attempted to run PostgreSQL.

The fix is (simplified Dockerfile snippet):

The key takeaway is to think about runtime actions (what cargo pgrx test will do) when you design the container user. Create and use a non-root user early.

The cargo permission trap

This one is subtle. The official Rust images set global cargo paths that default to /usr/local/cargo. If you install cargo-based tools (like cargo-pgrx) while still root, you may populate root-owned directories. Later, the non-root user cannot write to those locations and will get permission errors like:

The mistake is ordering. Installing tools as root and then switching to a non-root user leaves root-owned cache and registry files behind.

Correct approach would be:

  • Create the non-root user first
  • Set CARGO_HOME and adjust PATH to the user's home
  • Install cargo-pgrx as that non-root user so all cargo files live under /home/vscode/.cargo

Example:

Now downloads, registry cache, and installed binaries belong to vscode and are writable by the runtime user.

 

The pgrx state persistence problem

After fixing users and permissions, I still hit this error during cargo build:

I originally ran cargo pgrx init during the Docker build (image creation). That created /home/vscode/.pgrx/config.toml in the image. But when you start a devcontainer in VS Code, mounts or overlays (bind mounts, workspace folders, or explicit mounts) can hide that file. On Windows, bind mount path resolution is especially fragile. The result is that the running container doesn’t see the config produced at image build time.

The initial attempt failed because a bind mount will replace whatever is at /home/vscode/.pgrx with the host directory contents. If the host path is empty or different (Windows path translation!), you lose the image-built config.

The final approach that worked is to use a Docker volume for cargo registry cache so we keep speed benefits and run cargo pgrx init in postCreateCommand, so initialization happens when the container is live and the runtime user can write to their home directory.

Optionally use --pg17 download to fetch prebuilt PostgreSQL binaries for pgrx instead of building PostgreSQL from source.

Example devcontainer.json bits:

Key takeaway is that some tooling must be initialized at runtime, not at image build time.

Bonus: Docker-in-Docker and testcontainers

If your test suite uses testcontainers (Rust library) to spin up Docker containers for integration tests, you’ll need Docker access from inside the devcontainer. Rather than running Docker-in-Docker, I prefer to reuse the host Docker socket via the docker-outside-of-docker devcontainer feature. This keeps things simple on the host and avoids nested Docker complexity.

Example feature entry in devcontainer.json:

Also make sure your container user has permission to access the Docker socket when needed. On Linux, that often means adding the user to the docker group. With the feature, VS Code handles most of that wiring.


The final working configuration (condensed)

Below is the condensed configuration we used. For the full version, check etcd_fdw repository. This is not a drop-in for every project, but it captures the important patterns.

Dockerfile (important parts):

devcontainer.json (important parts):

Notes:

  • --pg17 download instructs pgrx to fetch a prebuilt PostgreSQL 17. It saves time when you don’t need to build PG from source.
  • The cargo registry cache is kept in a Docker volume for cross-platform reliability.

Short troubleshooting checklist

If something breaks, check these in order:

  1. Are you running as a non-root user in the container? remoteUser should be a regular user.
  2. Does ~/.pgrx/config.toml exist inside the running container (not necessarily in the image)?
  3. Is the cargo cache owned by the runtime user (/home/vscode/.cargo)?
  4. If you use bind mounts from Windows, verify the host path resolves correctly to a Linux path.

Takeaways

  • Think about runtime actions, not just build steps. pgrx starts PostgreSQL at runtime. Design accordingly.
  • Order of operations matters. Create users before installing user-specific tools.
  • Prefer volumes for cache and do initialization with postCreateCommand.

Final words

Setting up devcontainers is a small engineering puzzle. For pgrx, the puzzle pieces are: PostgreSQL security, cargo ownership, and initialization timing. Align those and you get a pleasant, reproducible developer experience. 💙💛

Image

Leave a Reply

Your email address will not be published. Required fields are marked *

©
2025
CYBERTEC PostgreSQL International GmbH
phone-handsetmagnifiercrosscross-circle linkedin facebook pinterest youtube rss twitter instagram facebook-blank rss-blank linkedin-blank pinterest youtube twitter instagram