Local Development with Docker and Podman on Linux

Docker and Podman let you run project dependencies on Linux without installing every database, language runtime and background service directly on your workstation. A project can carry its application environment alongside its source, making setup easier to repeat and mistakes easier to discard.

The useful distinction is not simply Docker versus Podman. It is what runs on the host, what runs inside containers, where persistent data lives and which privileges the container engine receives. Those decisions determine whether containers simplify development or add another place to troubleshoot file permissions.

This guide covers local application development with either engine: choosing an approach, preparing rootless operation, arranging a Compose project, handling storage and networking, and maintaining a repeatable daily workflow. For editors, Git and the rest of the host environment, start with setting up a Linux development workstation.

What Containers Should Do for Your Development Environment

A container image supplies the application filesystem and its packaged dependencies. A container is a running or stopped instance of that image, with runtime settings such as mounts, networking and environment variables. Keep those responsibilities separate: the image describes the software, while runtime configuration describes how that software connects to your project.

Linux containers use namespaces to isolate resources such as process IDs, mount points and network stacks. The Linux namespaces manual documents these boundaries. Containers running directly on Linux still use the host kernel; they do not boot a separate kernel for each application.

That makes containers useful for running two projects that need different database versions, testing an application against a clean dependency set, or giving teammates the same starting environment. It does not make the host distribution, CPU architecture or kernel irrelevant.

Use containers where they remove a real source of variation. A small command line utility may need only a language environment and a test runner. A web application with a database, worker and message service has more to gain from a declared container setup.

A virtual machine serves a different purpose. Choose one when you need another kernel, a complete operating system installation or a stronger separation boundary for risky experiments. The guide to running Linux virtual machines with KVM and QEMU covers that approach. Shared folders and network access still deserve scrutiny inside a VM.

Choosing Between Docker and Podman

Choose the engine that fits the project’s tested workflow, not the one with the most persuasive logo. A repository maintained around Docker Compose should work there before you attempt another runtime. For a new Linux project, rootless Podman is also worth evaluating, particularly when you want container management under your regular account.

Docker Engine uses a daemon to manage containers. Podman can manage local containers without a central, continuously running management daemon, although integrations can use its API service. Both handle OCI compatible images. These architectural differences are covered in Red Hat’s container tools documentation; they do not imply that every surrounding integration behaves identically.

Questions to resolve before choosing a local container engine
Decision Docker approach Podman approach
Existing project support Use the engine and Compose release tested by the repository. Check the repository’s Podman support rather than assuming compatibility.
Rootless operation Configure and verify a rootless daemon. Check the regular user’s rootless configuration and prerequisites.
Compose workflow Confirm the Docker Compose installation works with the chosen engine context. Identify and test the external Compose provider.
Editor and test integrations Verify which daemon endpoint each integration uses. Verify whether the integration supports Podman directly or needs its API socket.
Team portability Record required features and versions. Test the same project operations before declaring both engines supported.

Do not replace the Docker command with a Podman alias and assume the migration is complete. Image builds, Compose features, socket discovery and user mappings all deserve testing. A successful image download proves much less than a successful development workflow.

You can keep both engines installed while evaluating them, but assign each project a clear default. Avoid switching engines halfway through a debugging session unless the engine itself is what you are testing.

Prepare the Host Before Starting a Project

Keep Installation Sources Consistent

Use installation instructions that match your distribution and release. Decide whether the machine will use distribution packages or an upstream package repository, then keep that choice documented. Mixing installation methods makes later upgrades and troubleshooting harder than necessary.

Check the engine, Compose implementation and any required networking or storage helpers as separate parts of the setup. Installing a container engine does not prove that the repository’s entire development workflow is available. Our guide to APT, DNF and Pacman covers the host package management side without tying your project to one distribution.

Before adding application services, verify a small disposable container under the account you intend to use every day. Confirm that image retrieval, process startup, networking and cleanup work. Resolve host configuration problems at this stage rather than after introducing a database and several application services.

Verify Rootless Operation

Rootless Docker runs both the daemon and its containers without host root privileges, using a user namespace. This differs from running an ordinary user inside a container managed by a rootful daemon. Docker’s rootless mode documentation covers the required mapping helpers, subordinate user and group ID ranges, installation process and verification.

For either engine, check the user namespace configuration, required helpers and networking prerequisites before adopting rootless operation. Record the result in the workstation setup notes. Do not assume that a successful operation under an administrator account validates the regular user’s environment.

If Docker uses a user service, distinguish that service from a system service when investigating startup problems. The same distinction matters for socket based integrations. The guide to managing Linux services with systemd explains service ownership and lifecycle management.

Treat Engine Access as a Privilege

Control of a rootful Docker daemon can permit changes to the host through container configuration and mounts. Treat its socket, and any group that grants access to it, as an administrative boundary. Mounting that socket into a development container is not an innocent convenience. Docker’s engine security documentation describes this attack surface.

A read only filesystem mount of an engine socket does not turn the API into a read only API. Avoid exposing it unless the integration genuinely requires it and you trust the software receiving access.

Rootless operation reduces host privilege; it does not protect files you deliberately make writable inside the container. Review project mounts with the same care you would apply to a local program accessing those directories. Keep broader account permissions deliberate too, rather than using sudo and group membership as general fixes for container errors.

Decide What Belongs Inside the Containers

There are two sensible starting arrangements. In a hybrid setup, the application runs on the host while services such as the database run in containers. In a fully containerized setup, the application, supporting services and test tools run in containers, with the editor usually remaining on the host.

The hybrid approach suits a developer who wants straightforward debugger integration and already has a reliable language environment. A Python application, for example, can use a host virtual environment while connecting to a containerized database. Keep that host environment reproducible with the practices in managing Python versions and virtual environments.

Putting the application in a container makes more sense when system libraries, native extensions or team consistency are the main concern. It also gives you another boundary to account for when configuring debugging, file ownership and source reloads.

Neither arrangement is inherently more professional. Choose one explicitly and document it. Avoid a project where tests use the host runtime, the development server uses a container, and nobody knows which dependency set the editor is checking.

A Practical Application Layout

Consider a hypothetical web application with an API, a background worker and a PostgreSQL database. Keep the design small enough to understand before adding optional services.

A suggested local development layout
Component Suggested arrangement Persistence decision
API Host process or application container, chosen consistently Source remains in the working tree; generated output has a documented location
Worker Separate service using the same application dependency set Keep durable work state outside its disposable filesystem
Database Dedicated container using a reviewed image version Use a named volume and provide a deliberate reset procedure
Tests and migrations Repeatable project tasks using the application environment Separate test data from development data where practical

Do not add a cache, reverse proxy or message broker until the application needs one. Each extra service adds startup behavior, configuration and another possible explanation for a failed request.

Use Compose as the Project’s Operating Description

A Compose file declares services and their runtime configuration. It can describe images or builds, mounts, networks and other application requirements. The Compose Specification’s service model provides the common vocabulary, but the installed implementation still determines which features your workflow can use.

Keep the project’s Compose file, image build definition, dependency lockfiles and setup documentation together in version control. Use project relative paths rather than one developer’s home directory. Provide an example configuration containing harmless development values, not working credentials.

Separate shared project settings from personal preferences. A developer may need another browser port or debugger setting without changing the team’s database configuration. Make those adjustments through a documented local override mechanism rather than permanent edits that keep appearing in unrelated commits.

Podman Needs a Compose Provider

Podman’s Compose integration delegates work to an external provider. Its wrapper prepares the environment and passes operations to a tool such as Docker Compose or podman-compose. The Podman Compose manual documents provider selection.

Record the provider and version you test, not just the Podman version. Two machines can have the same engine but use different Compose implementations. Make provider selection explicit when the project depends on one.

Before advertising support for both Docker and Podman, test image builds, startup, service communication, health checks, migrations, test execution and cleanup on both. Prefer the common feature set unless an engine dependent feature solves a clear problem. Document exceptions beside the configuration they affect.

Keep Source Code, Dependencies and Data Separate

Use Bind Mounts for Files You Edit

A bind mount makes a host directory available at a container path. It is a natural choice for source code because your editor and the container work with the same files. A named volume gives the engine responsibility for storage and is often a better fit for database state.

Choose mounts according to the files’ lifecycle. Source belongs in the working tree. Rebuildable dependencies belong in an image or a documented cache. Durable database contents need persistent storage. Test output needs a known destination that remains accessible after the test process exits.

Keep container paths predictable across the project. The application’s working directory, debugger mappings and test configuration should agree. If host and container directory layouts are becoming difficult to follow, review the basics in the Linux filesystem and directory structure guide.

A mount can also hide files already present at its destination in the image. If dependencies installed during the build seem to disappear at runtime, inspect the mount layout before rebuilding repeatedly. Mount the directories you need rather than covering the entire application tree without considering what the image placed there.

Resolve File Ownership Deliberately

Linux checks numeric user and group IDs, not whether account names look similar. A container user and a host user can share a name while having different IDs. User namespaces add another mapping between the two environments.

For writable source mounts, decide which identity should create files and verify the result with a disposable file. Check that both the editor and the application can modify it. Do this before generating a large dependency directory.

Podman’s keep-id user namespace mode can map the invoking user to matching IDs inside the container. It can help with source mounts, but it also affects the identity used to start the container and may not suit every image. Its ownership adjustment options need care: recursively changing a mounted directory changes host files.

Do not solve an ownership mismatch by granting write access to everyone. Diagnose the host owner, container identity and namespace mapping. The Linux file permissions and ownership guide covers the underlying checks.

Handle SELinux Labels Without Disabling Protection

On an SELinux enforcing host, ordinary ownership and permissions may be correct while a label still prevents access. Podman supports shared labeling with the lowercase z mount suffix and private labeling with uppercase Z. Its container run reference documents labeling, user mappings and ownership options.

Apply relabeling only to suitable project directories. Do not recursively relabel your entire home directory or system paths to make one application start. If several services need the same source directory, account for that sharing when choosing the label.

Keep ownership changes and labeling changes distinct during diagnosis. Changing both at once may make the application work while leaving you uncertain which adjustment was needed.

Give Database State an Explicit Lifecycle

Use a named volume when you want development data to survive container replacement without tying the project to a manually selected host database directory. Confirm the correct data mount destination for the chosen image release instead of copying a path from an unrelated example.

Write down three separate procedures: stop the project, recreate its containers, and erase its data. They should not be interchangeable. Make the destructive procedure unmistakable.

Give parallel projects distinct storage identities. If two branches require incompatible schemas, decide whether they should use separate project instances or separate databases. Sharing one writable database between unrelated experiments is an efficient way to create an inefficient afternoon.

Make Networking Explicit

Think about three locations: the host, the application container and a dependency container. An address that works in one location may refer to something else in another.

With separate network namespaces, localhost inside the application container refers to that container’s network namespace, not the host or a separate database container. Services sharing a Compose network can normally communicate using service names. Prefer those names to hard coded container IP addresses.

A browser on the host instead needs a host reachable endpoint. For the example application, publish the API on a loopback address and leave the database unpublished unless a host tool needs direct access. Docker Compose’s service configuration reference documents service networking, host address bindings and dependency health conditions.

Separate the Application Listener from the Host Binding

Inside its container, a development server may need to listen on all container interfaces so published traffic can reach it. Separately, the published host port should bind to loopback when only your workstation needs access. These settings operate at different boundaries.

Check the actual listener and published address rather than assuming that an application called “local” is private. Keep database consoles, debugger endpoints and development administration pages off public interfaces.

If access from another device is intentional, make it a separate documented mode. Review authentication, test data and the host’s Linux firewall configuration before exposing the service. Do not widen the binding as a routine response to a failed browser request.

Distinguish Startup from Readiness

A started database process may still be initializing. Compose dependency ordering alone is not a guarantee that an application dependency is ready. Where supported by the implementation, a health check combined with a service_healthy dependency condition can delay a dependent service until the check passes.

Choose a health check that tests the operation the dependency must provide. Keep bounded retry behavior in the application too. Startup coordination does not replace recovery when a database restarts later.

Treat migrations as a deliberate project task. Define when they run, how failures appear and whether repeating them is safe. Avoid hiding migration errors behind an application restart loop.

Build Images That Are Easy to Rebuild

Start with a maintained base image that suits the application’s runtime and library requirements. Choose readable version references rather than relying on an unexplained latest tag. Tags can move; digest pinning identifies the selected image content more precisely, but requires a process for reviewing updates.

Keep the build context small with an appropriate ignore file. Exclude credentials, local database files, editor caches and other material the build does not need. Use separate build stages where that helps keep development tools out of a runtime image. Docker’s image building best practices cover these techniques and their maintenance tradeoffs.

Arrange the build so ordinary source edits do not unnecessarily repeat dependency installation. Then verify that the build also works without relying on a developer’s existing cache. Fast local builds and reproducible clean builds are separate goals.

Keep dependency installation in the declared build or project workflow. Installing a missing library interactively inside a running container may help diagnose a problem, but it is not the finished fix. Put the change into version controlled configuration and recreate the environment.

For development, favor quick source reloads, readable logs and debugger access. For deployment, make a separate decision about included tools, source mounts, credentials and startup behavior. A local development server is not a production web server; the guide to hosting a website with Nginx and HTTPS covers that different responsibility.

Establish a Repeatable Daily Workflow

A project should explain normal operation without requiring its original author to stand beside the keyboard. Keep the workflow short enough to follow, but precise about which environment performs each task.

  1. Review configuration. Check image changes, required local values and any migration notes after updating the repository.
  2. Confirm the engine. Verify the intended rootless or rootful environment and Compose provider before changing project state.
  3. Build and start. Rebuild when dependency or image configuration changes, then inspect startup results.
  4. Prepare application state. Apply migrations and load only the development fixtures the project expects.
  5. Run checks in the declared environment. Use the project’s chosen runtime for tests, formatting and other checks.
  6. Stop without accidental deletion. Preserve development data unless you deliberately choose the documented reset procedure.

Give one off tasks the same dependency set and configuration as the application where practical. A test that passes only because the host has an extra library installed is not validating the container environment.

Keep logs attached during initial setup and unfamiliar failures. Once startup is reliable, detached operation may be more convenient. Either way, document where developers should look for the application, database and worker output.

Validate onboarding with a clean checkout and newly created project storage. Confirm that a developer can reach the application, run tests, restart the project without losing data, and perform an intentional reset. Existing images and volumes can conceal missing setup steps.

For teams using both x86 and ARM workstations, include architecture in this validation. Do not infer equivalent behavior from the same image tag alone. Test the application path that each supported machine will actually use.

Troubleshoot the Failing Boundary

The Application Runs but the Browser Cannot Reach It

Check whether the application process remains running, whether it listens on the expected container interface, and whether the intended port is published. Then check the host binding and browser address.

A successful request from inside the container narrows the problem but does not prove that host access works. Avoid changing application code until you know which connection fails.

The Application Cannot Connect to the Database

Inspect the database logs and readiness state before changing credentials. Confirm that the application uses the right hostname for its location: a host process and a containerized process may need different connection settings.

Then compare database name, user, credentials and migration state. Distinguish name resolution failures, refused connections and authentication failures; they point to different causes.

Files Are Missing, Stale or Unwritable

For missing files, inspect the mount source and destination. For write failures, check ownership, user mapping, read only settings and SELinux labels. For stale reloads, confirm that the container sees the edited file before investigating the application’s watcher.

Change one condition at a time. A rebuild will not repair a mistaken host path, and broad permissions will not explain why the wrong directory was mounted.

Containers Seem to Have Disappeared

Check the selected engine endpoint, user account and rootless mode before recreating anything. Rootless and rootful Podman use different storage locations, and different engine environments can show different resources.

Also check whether a project name or working directory changed. Recover the intended context first; otherwise a second setup may obscure the original data.

The Workstation Becomes Slow or a Service Exits

Measure CPU, memory and disk activity before changing engine settings. Investigate repeated application rebuilds, busy file watchers, database activity and accumulating output. The Linux resource monitoring guide provides a broader diagnostic workflow.

Read service exit details and logs rather than assuming every abrupt termination is a memory problem. If the failure belongs to a user service, engine service or socket rather than the application, use journalctl troubleshooting to inspect that layer.

Maintain the Environment Without Losing Useful State

Classify stored material before cleaning it up. Images, build caches, stopped containers and persistent volumes have different purposes. Rebuildable material can usually be treated more aggressively than a database containing work you want to keep.

Review deletion options before approving cleanup. Avoid broad storage pruning as a standard troubleshooting step. It can remove useful evidence along with material you later need to restore.

A persistent volume is not a backup. If development data matters, export it through a database appropriate procedure and test restoration into separate storage. Include the resulting backup files in your desktop backup and recovery plan, with access controls suitable for their contents.

Schedule image and dependency reviews alongside host maintenance. Updating the workstation does not automatically rebuild application images with refreshed packaged libraries. Keep the host patched through a deliberate Linux security update process, then rebuild and test project images separately.

The right local setup is the one your project can recreate, explain and repair. Choose Docker when that is the tested team workflow; choose Podman when its operating model fits and the required integrations pass validation. In either case, keep privileges narrow, data ownership explicit and destructive actions separate from everyday development.