The Slippery Slope of Docker Dev Environments
blog.testdouble.com
blog.testdouble.com
1. Developers can now help you debug that one issue in prod that no one can figure out.
2. Deployment is as easy as adding a line to a config (load dev yaml + patch)
3. Good devs will start automating the boring stuff. Good operators will look at, learn to use, and love these tools.
4. Developers will make sure containers and roll out systems work lightening fast.
5. "We need to run this migration in production. Tested it locally" actually means something now.
6. Demos can be done locally with absolutely no network connection. Demoing to the security minded? Worried about showing off your, often down, production env? No problem! Bring a dev laptop.
7. Developers can test QPS quotas, build dashboards for metric collection, benchmark performance, etc
I think it's odd to get hung up on prefixing your commands with "docker-compose exec" or putting "docker-compose up -d --build" under watchman/inotify or investing in setting up minikube + (tilt | skaffold).
Sure it's more stuff for your developers to know but it's very powerful to see your change as it effects the entire living system.
Every company I’ve ever worked for, to setup a dev environment and get the product passing test suites has been days to weeks. And without the automation and standard operating environment, every release was filled with missing dependencies because developers only tested on their local precious system. A side benefit to moving to containers is not contaminating your own machine with libraries or even breaking it because of versioning or config issues.
Docker gets developers and production machines on the same page.
... I would never go back to bare metal development
Instead of reading this article as "docker containers are bad" (wrong), try reading it as "there are problems with docker containers that can be improved" (right).
Containers fix a large number of issues, and many of these fixes directly benefit developers. But the article is right to point out inefficiencies that docker introduces in developer workflows. Docker may be taking developers and ops three steps forward, but for some, it's also one step backwards.
It's night and day compared to the Javascript world, where it's nearly a given that the bare minimum should be watchers that recompile code and restart a server before you finish switching window focus.
It works extremely well for language communities that need this, in particular it caught on because you would build something and test it locally and it would work just fine, but then you are running the wrong version of Python or pip installed the wrong version of a library, or you have the wrong version of PHP or Node.js relative to Prod.
For Java and Go, I believe they have already settled these problems, no? You might shell out to other commands and have a weak dependency on your runtime environment that way, but if you stay within your lane, then you already have an executable with all of its dependencies bundled in. They just have a lot less to gain from Docker. I can't see why they would have more to lose.
I mentioned it further down, but my team and I are having preliminary success by integrating with VSCode's "Remote: Docker" extension. They've got it rigged so that all language support can run as remote servers inside the container. They give you a special .json file to control how you'd like to start the image, and you can supply a project-specific dockerfile that takes any additional IDE-specific steps that you want to add in as layers when opening up the project's dev container.
It still has some rough edges - for some reason, you can't supply Build-time arguments to the devcontainer Dockerfile I mentioned above, which makes it hard to get permissions to be non-annoying if you're worried your users will have odd UIDs. And there are a couple other random ticky tack things. But it's by far the closest to actually "developing out of a container" as you and I are imagining it, with all of the attendant benefits. When you want a new dev to start working on the project, they basically do this:
1. git clone newproject 2. Open that project in vscode 3. Respond to a pop-up that yes, I do want to open this in a container 4. <wait for the dev-specific layers to be built> 5. Do a normal project build 6. Enjoy full intellisense, syntax highlighting, import resolution, etc. in exactly the environment that's going to run.
In addition, by including the Dockerfile that builds the base image (the one that the dev container FROM's) into the source, developers can now change library versions and dependencies as a git commit, and then coerce other developers into automatically updating their environments by pushing a new image with a new tag to your gitlab / w/e, and then updating the tag mentioned in the FROM in your .devcontainer/Dockerfile. It's like how you would expect development to feel in 2020.
All the times I can think of when we've run into "works in dev, not in prod" issues that weren't pilot error, it has been down to stuff like firewall configuration or the fact that production is distributed across more nodes than people can realistically simulate on a single dev machine. Never once that I can remember have we had a single production problem caused by the JVM working differently in dev environments.
It is pretty telling that if you're looking online for tutorials about how to effectively use Docker in a development workflow, you will find essentially none that involve running a compiler.
For exploratory programming I used to just enable remote debugging and set break points. This should be going away in favor of the REPL introduced in Java 9 tho... Haven't met lots of developers that have integrated it into their gradle builds yet tho...
How do you do exploratory programming in languages/environments that don't give you a remote REPL? (For instance, how do you do it in C?)
If I had to do exploratory programming without a REPL I'd probably leverage my test environment to write small bits of code, step through them with a debugger, and then use the results to structure a larger program.
To more quickly edit configs just make it so your containers build and start quickly. This is very easy to do if you are aware of the docker layer cache.
Hot reload is basically `while (true) build() && restart()`
Also, now your gdb setup isn't containerized, and that is genuinely part of your "dev environment" (it's a thing you want to share with other developers), so you lose out on many of the advantages of Dockerizing your dev environments. Note that you can't gdb/strace across containers because you can't see pids in one container from another container. (Similarly, a Kubernetes sidecar with gdb/strace isn't really going to work.)
Not all of my software restarts quickly, and it's not because I don't know how to make containers do things - it's because the underlying build system is slow even on the local machine, or because they do a lot of processing at startup, or whatever. (Very few of the things I work on are webapps!)
In Kubernetes, you can configure a Pod to share a single process namespace among its containers.
https://kubernetes.io/docs/tasks/configure-pod-container/sha...
I've found that given the many docker specific issues I've ran into in development, the "matches your production" argument doesn't always bear completely true unless you're also using containers in production.
You can have these running on the same stack as your prod (for instance in a similar kubernetes cluster, or at least on a mirrored config by the same provider if you’re on plain VMs), getting that much closer to test/prod parity.
Most companies can afford one virtual env per dev, so the remote test environment becomes an extension of their local one. Demoing features offline should still be OK as well, you need everything setup locally for dev anyway.
If the goal is to get people used to what’s running in production and train them to access the containers and debug them remotely, it’s another beast altogether, and reminds me of all the discussion about learning vim. It brings tremendous value to be proficient in coding in CLI, and not just for simple edits, but to actually be able to use it as the main coding tool.
But then is it an efficient time investment for most people ? It’s pretty close to living your life in dim light because one day you’ll have to deal with a blackout and you’ll be super efficient then. It’s good to have a few people doing that, I’m not convinced it’s good for a whole team.
I agree that docker provides many efficiency improvements, especially with respect to testing, reproducing bugs, and deployment.
But what about pure development of new features? In a bare-metal development environment, you just compile and run or launch a debugger. With docker, you need to compile, build a docker image, run the docker image, then launch your app/debugger. Would love to hear solutions on how to eliminate/ameliorate these additional steps.
If you are working with a scripting language that can be enough to have your changes reflected on the running instance, otherwise you can set a hook on these changes to recompile and/or redeploy the new code automatically.
We're already doing "bundle exec rails s" or "bundle exec capistrano production deploy" commands. So a "docker-compose up" shouldn't matter at all.
To be fair, I've had frontenders complain that it was hard for them to remember the capistrano deploy commands. And that they'd love a GUI butten for deployments. Rather than just look at our wiki or write it down.
For the few frontend devs that need to deploy the applications it's way easier for them to just save the command to their notes.
But, yes, putting the how to deploy into the README.md can be done in 5 seconds whereas setting up a CI/CD pipeline is a lot more effort. So I guess frontend devs just have to be tamed :)
As for everyday running of the actual software you’re writing: no way. The tooling for integrating an editor or IDE with the container process is pretty poor (so one often ends up just setting up everything on the host anyway), image rebuilds are frequent and slow, and most language ecosystems already have tools for installing and switching between different versions. They are useful if you want to more closely replicate your CI or production environment for specific debugging, though. Nevertheless, the belief that running a container locally will “never create disparity between your development environment and your production environment” is a farce. There are so many other variables that are often still different. It helps, but it’s far from a silver bullet.
> the developer environment just doesn’t square well with whatever system is running your code. In the olden days of yore, this disparity was so great that we often had to tunnel in to an environment that was exactly like our production server
For web apps, I've gotten stuff working very easily across linux, freebsd, and mac os. Thanks a lot due to package managers, up to date library packages, and any python/ruby/node packages with c/c++ parts generally being able to find the header/.so files. Lockfiles (package-lock.json, poetry.lock) have gotten better as well, so I feel secure knowing that there won't be hiccups there.
Thank you for your comment on docker CLI.
I find docker commands more confusing and obtuse to work with than the commands of postgres, python, node, webpack, combined. All of them. Because it's about having to think about copying files, proxying against the file system, layers of containers. By the time stuff is running I feel so far away from the roots, I'm asking myself how much of a favor is this really doing?
The other thing is deploying these images is a lot of work compared to just running SSH commands via fabric, pulling code, and restarting services. I also think developers benefit from jumping in and wiring in the daemons themselves on whatever system. I think it's part of what fullstack is. It'll come in useful should stuff go wrong in production.
I also found my experimentation with docker inconsistent, even when I cloned projects from git repos with docker compose, I have a high failure rate. To the point I'd be happier if they just gave instructions for me to wire in the daemons and db myself. And when I google, I go down a rabbit hole of configurations and github issues that don't seem to fit (perhaps docker or docker-compose is out of date somehow? Or is it the project itself? Or me?)
My questions is, how the hell is anybody getting anything done _without_ all the features I just mentioned? I've found initial, paltry support for particular languages in JetBrains, etc. IDEs, and that's about it. None of the other IDEs have any real solution go getting multi-language intellisense from inside the container. Are people just like... exec'ing into the container and then dicking around in Vim? Or are people mapping all of the source in as a volume, running the IDE outside the container, and just forcing the user to have all the relevant dependencies installed on their host system if they want to see auto-completions, go-to-definition, etc. (thus completely defeating the purpose of containerizing your environment)?
In my eyes, the only reason Docker as a dev environment makes any sense at all is because of the uniquely good support vscode offers, but I see it mentioned all over the docker website that it can help you make your development environment repeatable and exactly like your production environment. But... that doesn't actually work, like, at all...
Can somebody help me understand what I'm missing?
you map the source directory as a volume and then run the editor outside the container.
My prediction is that you might be using a stack where it's not a big deal to isolate project-specific dependencies, npm or virtual-env style. And so in that context, it's not a big deal to ask developers to do a pip install -r requirements.txt or whatever, and then the docker container's job is just to emulate the runtime environment.
It has just always made sense to me that the win of dockerizing your dev environment would be that you could reproduce it on another machine with a docker pull, so those sort of "half-way" workflows were unsatisfying to me. I'm always curious to hear how other people are doing it though - thanks for responding.
we have a shell script that adds some syntactic sugar around running docker in a consistent way, but other than that it's left to the dev how they want to manage the editor process.
Personally, I don’t care for auto-completion or go-to-definition, but I want to be as sure as possible that if my software doesn’t break in development then it won’t break in staging and production.
>Personally, I don’t care for auto-completion or go-to-definition
What about just plain old syntax highlighting to make sure your imports are resolving? Do you just run vim from inside the container or something, and then find out whether it builds / works by running the container?
Like, say you had a Python project that you were going to containerize. Do you perhaps do the actual editing of the source on your dev machine, with all of the relevant libraries installed into a virtualenv or something? If that's the case, then it's basically a guess-and-check workflow, right? There's nothing strongly enforcing your virtualenv to match what's in the container. And there's nothing about having the container that saves you from having to setup your local environment in a way that's suitable for that particular project. That just doesn't seem like "developing out of a container" to me. More like... using containers as a local test environment, or something like that.
Maybe you're just explaining why the Docker folk feel it's reasonable to say that Docker improves the development experience, putting aside whether they imagine you literally sync'ing up your IDE up with a container as I was describing.
What I'd really want is a system that takes nix files (see Nix & NixOS), and gets that running as a container. Or something like that: Guix or whatever else. Free package-level caching and deduplication, and so on.
For a push approach, I am maintaining this project: https://github.com/nix-community/docker-nixpkgs
Both work well for CLI-like applications. What is missing is to get something similar for services where there is a mix of configuration coming from environment variables and files.
https://grahamc.com/blog/nix-and-layered-docker-images
It's far from a native solution, and is a bit hacky due to the 256 layer limitation in docker, but it's most of the way to what you want, I think.
See docker-image and container here: https://guix.gnu.org/manual/en/html_node/Invoking-guix-syste...
If you are using only one system, the NixOS containers support might be enough, though.
https://github.com/NixOS/nixops/issues/565
It has links to solutions people are using, and other related issues.
As soon as you have multiple projects with a mix of dependencies (especially when you’re dealing with incompatible versions or obsolete packages) it really becomes a huge win. Doubly so when you have out of date dependencies that (for whatever bad reason) you can’t update, but also don’t want leaking out into your dev environment.
How do you test code quality and code security as part of your pipeline for all of those different versions on the fly ?
eg some run python 2.7 and some run python 3.0
a. Code quality - tackled at code review time, and probably not dependent on python 2.7/3.0 differences; if they are version dependent then again it's a code review process. b. Code security - again, code review
Additional:
c. Third party dependency vulnerabilities: detection should be automated; github now supports automatic checks that open pull requests for vulnerable artifacts.
d. user acceptance tests - seems unlikely that two versions of python are used for the same part of the system.
Docker (especially docker-compose) makes it more visible as to what will be deployed. In fact, it may actually be better for security when those security checks of a system can be made before it goes to production, because the whole stack can be built, audited and executed locally.
Our corp requires this at a minimum to even be deployed to development. Let alone staging / prod
Regardless of the downstream testing/integration concerns, I work with projects with multiple environments, ranging from current Rails apps to a Rails 2 application (that lives in a VERY isolated environment as an internal-app for a cheap "customer"), with various things in between. I also contribute to some open source projects that want weird environments.
I've got those projects on my plate regardless of your downstream concerns (which are very valid). However, Docker lets me work on each of them in isolation from one another, and also in a way where none of the goofiness is leaking out into my dev workstation.
Prior to spinning it up in Docker, the Rails 2 app I mentioned could only be worked on in production because setting up a dev environment for it that didn't require removing all the modern stuff was effectively impossible. So while I'm embarrassed to still support that project, at least I have a way to do so and a transition path forward.
Containers provide consistency and isolation. I dont think container, as an abstraction, is needed at the development stage.
Once development is done, then you pass around a resultant container.
For example, I run my Rails projects directly on my Mac (Rails + Native MySQL), my coworker on Linux runs it there natively as well. That means we have to deal with runtime platform issues, true. However when we're ready to "release", we build a container that is then tested, and released to prod.
https://gitlab.com/verbose-equals-true/django-postgres-vue-g...
The readme for this project addresses some of the issues that you might have with integration testing. I use GitLab CI and I'm able to take advantage of GitLab runner to simulate my CI environment in containers that run on my local machine.
I explored a few different ways of running integration tests in my development environment, one using docker-compose with docker in docker, and another method using services in GitLab CI. I learned a lot from https://gitlab.com/gitlab-org/gitter/webapp.
We basically had Kube in prod, so worked backwards to make everything match that as closely as possible, solving the required problems along the way. docker compose for example is less likely to be used in prod and thus a poor choice.
Instead of opening four terminals and running ./manage.py runserver, npm run dev (twice) and hugo serve, and having project instances of Postgres, Redis and Celery running all the time, I just run docker compose up.
I want to move to a new machine? docker-compose up.
docker and docker-compose address blooming system complexity very well. I think that's more a reflection of the complexity than docker.
Sadly performance on Macs is woeful (due to architectural differences between the two systems) but even so it's still a very useful tool.