Elixir development using Podman with VM in Parallels and Shared Folders
Edits
April 17: Fixed an error with the nginx config for use with LiveViews
Motiviation
So recently I’ve wanted to give containers for development another go, but this time instead of using docker for Mac and it’s very slow file sharing to the containers give podman a try, since we are also using podman in production.
While the default mechanism using
podman machine
to run containers on Mac did in fact work out fine, and sharing my local development
folder with the containers worked fine, it also proved to be a rather slow
experience. That wasn’t a realy surprise given that podman machine
uses QEMU
in the background.
However what got me thinking is that podman remote
allows us to use any arbitrary Linux machine as the backend, so shouldn’t it be possible
to use a VM in Parallels instead?
This turned out to be possible, but a little bit complicated to setup, so what follows is a (hopefully) complete guide to setting this up.
Install VM
For this guide I am assuming that you already have Parallels installed on your local development machine.
So as a first step install podman
on your machine, personally I prefer to use brew
for this:
brew install podman
Next get a Linux to install as the VM in Parallels. You should be able to use any Linux, personally
I’ve decided to use Fedora, simply because
podman machine
uses this as well. However I’ve decided to use the server version and not CoreOS, which is used by podman machine
.
The reason for this is that setting up CoreOS is a bit more involved, and that we need to install
and run nginx
in a later step as well, and I don’t know how much effort it is to get that running
in CoreOS.
Now create a new VM in Paralles and choose to install from the image you’ve just downloaded.
Give the VM an easy name, I personally went with podmanhost
, and check the “Adjust settings
before installation” checkmark before continuing with the dialog.
Settings for VM
In the settings dialog that pops up after clicking the Next button, again make sure that the name is easy. Under settings I prefer the following settings (my Parallels is not running in english, so I’ll try to translate the labels of the settings to show the correct meaning):
Setings -> Start and turn off -> Automatically start in background
Setings -> File Sharing -> Share defined folders with Linux -> Select either your home folder or the folder where your projects live
Setings -> File Sharing -> turn off "Share Cloud folders"
Setings -> File Sharing -> turn off "Share Mac-Volumes"
Setings -> Programs -> turn off all checkboxes
Hardware -> 4 CPU`s, 4 GB RAM
Hardware -> Printer -> turn off all checkboxes
Hardware -> Network -> Shared network (as recommended)
Hardware -> Audio and Camera -> Do not share Camera with Linux
Then close the settings dialog and continue with the installation. Once the VM boots up, choose
the option Install Fedora
. Once the installer of Fedora has launched, open the menu for selecting the
required software. In there, choose the minimal installation
option on the left, and add
container managament
on the right side. Alternatively you can install podman later once the server is
installed. Next choose the installation target, I’ve simply chosen to let the installer setup
everything automatically here. For the root
account I’ve decided to leave it deactived, since we
do not need it (I plan to run the containers in rootless
mode), and finally we need to add normal
user. I’ve named the user podman
so that it is easier to remember later 😀.
Now we can start the actual installation and wait for it to complete, and then reboot the VM. Next I usually like make sure that all packages are up to date and that vim is installed, so run:
VM ># sudo dnf upgrade
VM ># sudo dnf install -y vim
Configure ssh with public key authentication from the host
Now in order to work more comfortably with the VM, I like to install a ssh key into the VM. In order to be able to copy it to the VM, allow password logins in ssh for now (if you do not know how to use vi or vim, just use any other editor):
VM ># sudo vim /etc/ssh/sshd_config
# and enable both of these lines:
PubkeyAuthentication yes
PasswordAuthentication yes
Afterwards reload sshd
and try to login. You should be able to use the name you’ve given the VM in Parallels
to connect, since Parallels should add an entry in your /etc/hosts
file with the name of the machine.
VM ># sudo systemctl reload sshd
# from your Mac
MAC ># cat /etc/hosts
# note: options should only be required if you are doing this for the
# n-th time to write a blog post on how to setup everything you've
# done like 5 times already :grinning:
MAC ># ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password podman@podmanhost
Next generate a key, setup the .ssh
directory on the VM and copy your public key to the VM.
# generate key and copy to clipboard
MAC ># ssh-keygen -t ed25519 -f ~/.ssh/podmanhost.shared
MAC ># pbcopy < ~/.ssh/podmanhost.shared.pub
# create ssh directory on VM and copy your public key into it (assuming you
# are connected using ssh)
VM ># mkdir ~/.ssh
VM ># chmod 700 ~/.ssh
VM ># vim ~/.ssh/authorized_keys
Then check if you can now connect to the VM from your Mac using the ssh key:
MAC ># ssh -i ~/.ssh/podmanhost.shared podman@podmanhost
# if you run into the problem that you get a "Too many authentication failures"
# disconnect, try this (again, if you've been doing this for 5 times
# already and are writing a blog post :roll_eyes:)
MAC ># ssh -i ~/.ssh/podmanhost.shared -o IdentitiesOnly=yes podman@podmanhost
If this works as expected, add an entry to your ~/.ssh/config
file so that you
can reconnect more easily later again:
Host podmanhost
PubkeyAuthentication yes
IdentityFile ~/.ssh/podmanhost.shared
User podman
And finally log into the VM again and disable password authentication again. One could argue if this is really needed since this is a just a local VM, however I prefer to never allow to let this setting slip so that this does not happen on a production machine by accident (I never allow password authentication on production machines!).
VM ># sudo vim /etc/ssh/sshd_config
# and disable this line:
PasswordAuthentication no
Again verify that you can still log into the VM from your host machine, and if that is all running fine we are finally done with the first steps.
Disable firewall
Next we are going to disable the firewall. Again this is something one should never ever do in production, but it is fine for a local development machine and makes our live easier later on with all the port forwarding that is going to be required.
VM ># sudo systemctl stop firewalld
VM ># sudo systemctl disable firewalld
VM ># sudo systemctl status firewalld
Install Parallels guest additions
Next we need to install the Parallels guest additions, so that we can use the folder sharing from the Mac to the VM, and later on into the container. The first thing we need to do for this is to insert the guest additions image into the VM.
At the top of the VM window click on the CD button -> Add image. The image we are looking for is located under Applications -> Parallels Desktop -> Contents -> Resources -> Tools -> prl-tools-lin-arm.iso. Check that the image has been inserted by clicking on the CD button again.
Next we need to switch to the VM and install a couple of libs and mount the image:
VM ># sudo mkdir -p /media/cdrom
VM ># sudo mount -o exec /dev/sr0 /media/cdrom
VM ># sudo dnf install -y gcc kernel-devel.aarch64 kernel-headers.aarch64 make checkpolicy selinux-policy-devel
VM ># sudo /media/cdrom/install
Follow the dialogs of the installer and wait for it to complete. Afterwards we need to reboot the VM:
VM ># sudo reboot
Now we should be able to verify the installation of the guest additions by checking
if the shared folders have been mounted into the VM. If so, there should be a folder
in /media
called pfs
, containing all the shared folders.
VM ># ls /media/psf
If the install was successfull, we can eject the image from the VM and continue with the next step.
VM ># sudo umount /media/cdrom
Optional: Disable SELinux
By default Fedora uses SELinux, and containers aren’t able to access the volumes
mounted into them. You can either pass the option --security-opt label=disable
to your containers when mounting them, or alternatively you can simply turn off
SELinux altogether. Again this is a no go on any production machine, but fine
on your local development machine. Just don’t ever mix them up 😎.
VM ># sudo vim /etc/selinux/config
# Change this line to SELINUX=disabled
SELINUX=disabled
VM ># sudo reboot
Setup podman from Mac to VM
For this we basically follow the manual from redhat.
First we need to setup podman on the VM so that it can be remoted controlled. Log into the VM and run these commands:
# __DO NOT__ run this as sudo
VM ># systemctl --user enable --now podman.socket
# now run as sudo again
VM ># sudo loginctl enable-linger $USER
Now we should be able to setup the podman remote
connection from our Mac host:
MAC ># podman system connection add podmanhost --default --identity ~/.ssh/podmanhost.shared ssh://podman@podmanhost
MAC ># podman system connection list
Now it’s time to test if the remote connection and podman is working:
MAC ># podman info
MAC ># podman pull docker.io/hello-world
MAC ># podman run --rm hello-world
If this prints out a hello world message, everything is working and we can remove the image again:
MAC ># podman image rm hello-world
Setting up Elixir development using shared folders
Configure port forwarding in Parallels
We’ll use the default ports for developing Elixir applications, so open the Parallels settings
and in Network -> Shared add a rule to forward port 4000 to podmanhost
to port 4000.
Later on we’ll need to modify this forwarding, but we’ll have a closer look at what to do when we then make the necessary additional setup to serve static files from the Elixir project.
Get a development container
Now it’s time to load your development container into podman. This will probably contain come additional tools like a gcc compiler etc, I might get back to how I prefer to setup my development container in a later blog post. For now we’ll simply use the (at the time of writing) current Elixir container. Note that we are not using otp-26 here, at the time of writing this there is a bug where something fails, but I can’t remember what it was.
MAC ># podman pull docker.io/elixir:1.16-otp-25
# Note that we are forwading from port 80 to 4000 here, that will change in a later setup
# --security-opt label=disable is required so that the container can access the volume with SELinux present
MAC ># podman run --name elixir_dev -p4000:4000 --security-opt label=disable --volume=/media/psf/podman/testing:/src -it elixir:1.16-otp-25 bash
Some note on the run command, first off the volume must point to the location of the shared folder
in the VM, not on your Mac, since the container is running inside the VM. Also note that
instead of using the --security-opt label=disable
you can also simply turn off SELinux
completely, as described above.
After running the command you should be dropped into a bash and check the contents of the /src
directory, which should in our case be the contents of the shared folder /podman/testing
.
For testing we will now create a new phoenix project without ecto
here (more on how to setup
a postgres connection later in this post), and try to compile and run it.
CONTAINER ># cd /src
CONTAINER ># mix local.hex --force
CONTAINER ># mix archive.install hex phx_new 1.7.11 --force
CONTAINER ># mix phx.new --no-ecto .
If you want you can time the download of the dependencies and the compilation time, and compare it to your native perfomance.
CONTAINER ># cd /src
CONTAINER ># time mix deps.get
CONTAINER ># time mix compile
Now make sure to change the config of the Endpoint
so that it listens to external connetions
as well. Open /src/config/dev.exs
and set the ip address to listen at to {0, 0, 0, 0}
, or
what is required depending on the http server backend you are using.
Now we should be able to run the server and make a connection. If you cannot connect, try to debug it by executing another bash on the container, then try to make the connection from the VM and finally from your Mac.
MAC ># podman exec -it elixir_dev bash
CONTAINER ># wget http://localhost:4000
VM ># curl http://localhost:4000
MAC ># curl http://localhost:4000
Unable to serve static files
Next you can try to download a static file like app.js
from the Elixir server. If this
works in your case, great! You are done and can either start developing or skip down to
the section where we setup postgres in a container as well. However in my case this did
not work because for some reason the connection is always closed prematurely.
MAC ># curl -O http://loaclhost:4000/assets/app.js
# output
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 764k 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
curl: (18) transfer closed with 783282 bytes remaining to read
Investigating this closer revealed a couple of interesting findings:
- When we serve the static files from a location on the containers own filesystem, the problem goes away and the files are received. However this would require us to have our working directory inside the container where it would not be accessible from the Mac, which is precisely what I do not want for my development setup. Here I’d like to use the editor etc installed on my Mac, and only use Elixir from the container.
- Trying the same from within the VM does not help either, the same error occurs.
- Using a different Linux variant like Ubuntu does not solve the problem.
- Installing another http server, in this case nginx, and trying to serve the files from the shared directory gives the same error.
However after some more trying around I did find out that setting the
sendfile
option of nginx to off
does solve the problem, and I can download the files
stored on the shared folder from the container.
Digging in deeper, if sendfile
is set to off
in nginx, the files are
written directly from the file system into the outgoing device on the container,
instead of first being buffered in memory by nginx. That set me off on a long
quest through the source of Erlang to try to find if there is a similar option,
however I was unable to locate anything that would solve the problem.
Close to giving up, I got an idea on how we can still make this work, which I’ll describe in the next section.
Using nginx to serve static files and Elixir for everything else
So the solution I came up with is this: In the build environment, the static
files are served directly from the priv/static
folder, by creating a symlink
in the _build/dev/lib/myapp/
folder back to priv/static/
.
Now we know the location of those folders in the VM, so instead of directly serving them from Elixir, we can setup a reverse proxy html server in the VM. That server can then serve all the static files, and forward all other calls to the container for processing by Elixir.
So in order to do this, a couple of more steps are required. Also note that I only tested this so far using the static files. If you need to serve more files from a different location that are stored on a shared folder from your host Mac, you will need to adjust the nginx configuration again for this. Also if you add such a mechanism in the future, you’ll need to adjust the config as well. This is not optimal, but should be manageable.
Installing and configuring nginx
Change port forwarding
Since we are going to use a reverse proxy, we’ll need to change the way we
handle the port forwarding. Instead of forwarding from port 4000
from
the Mac to port 4000
of the VM, and then again from port 4000
from the VM
to port 4000
of the container, we’ll instead use the default http port
80
on the VM.
So change the Parallels port forwarding configuration in Parallels settings ->
-> Network -> Shared. Change the forwarding rule to forward from port
4000
to port 80
on the podmanhost
.
Install nginx
Next we are going to intall nginx on the server.
VM ># sudo dnf install nginx
VM ># sudo systemctl enable --now nginx
# check if install is successfull
MAC ># curl http://localhost:4000
The later check should now fetch the default nginx landing page.
Disable SELinux
In order for nginx to be able to read the files from the shared folder we are going to disable SELinux like described in the section above.
As described above this is fine for our local development VM, but if you feel unsure about this and know more about how to work with SELinux feel free to leave it enabled and to grant nginx read permissions on the shared folders.
Configure nginx
Now we need to configure nginx as a reverse proxy and to serve the static files
of our project. For simplicity, given that this is only our development server,
we are going to configure everything directly inside of the main nginx.conf
file.
VM ># sudo vim /etc/nginx/nginx.con
First and most importantly, we need to swicht off sendfile
, so that nginx can
serve the static files at all.
# switch this line to off
sendfile off
Then in the server section, update the contents so that they look like this:
EDIT April 17: There was an error in the nginx config below where forwarding from the try_files
was not working with LiveViews. You must use a named location, and in the named location the
proxy_pass
line must not end with a dash.
server {
listen 80;
listen [::]:80;
server_name _;
root /media/psf/podman/testing/priv/static;
location / {
try_files $uri @elixir;
}
location @elixir {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwared-For $remote_addr;
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:4000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
}
And reload the config into nginx, after making sure that it is valid:
VM ># nginx -t
VM ># sudo systemctl reload nginx
Now we should be able to fetch a file from our Mac:
MAC ># curl http://localhost:4000/robots.txt
Afterwards make sure that your develpment container is running, and check that the forwarding is working as well.
MAC ># podman start elixir_dev --attach
CONTAINER ># cd /src
CONTAINER ># iex -S mix phx.server
MAC ># curl http://localhost:4000
And finally I would recommend to nagivate to http://localhost:4000
in
your browser to make sure that the html, Javascript and css files are
all loaded correctly. I.e. check that the website renders as intended.
Congratulations
Finally setting up the development environment has been fully setup 💯.
If you are interested in how to use postgres in this setup as well, continue on reading 🧐.
Setting up Postgres in a container for development
Basically in this section we will start the standard postgres container, and that’s it. Again I personally prefer if the DB is stored on a shared folder, so that I can more easily move the files around or switch to a different development setup in the future.
Optional: Shut down your local postgres instance
First we’ll dump any of our old databases from the local instance:
MAC ># mkdir -p ~/tmp/db
MAC ># cd ~/tmp/db
MAC ># pg_dump your_db > your_db.sql
Next we can shutdown postgres:
MAC ># brew services stop postgresql
Prepare the database directory
Create a directory where you can store your development databases:
MAC ># mkdir -p ~/tmp/podman/postgres
MAC ># chmod 700 ~/tmp/podman/postgres
Setup port forwarding for postgres
Since we are going to run our only instance of postgres in the container, we’ll use the default postgres port.
Parallels -> Network -> Shared add a rule to forward port 5432
to podmanhost
to port 5432
.
Create a named podman network for dns lookup between containers
I prefer to be able to use DNS names for communicating between the containers, so we’ll create a new network for our containers. As an alternative you can also consider enabling dns in the default network.
MAC ># podman network create development
MAC ># podman network inspect development
Pull the desired postgres image and initialize it
We’ll use one of the official postgres images from docker:
MAC ># podman pull docker.io/postgres:16
Next we run it for the first time, initializing the database. Note that in the command below
I’ve ommitted the security-opt label=disable
option, because we’ve
disabled SELinux altogether when setting up the nginx server.
If you did not do that, just add the option in the commands below.
For the database folder, we’ll simply mount that to the default location of the postgres data directory.
Notice that we need to pass the userns=keep-id:uid=999,gid=999
option in order for postgres
to be able to access the folder.
Finally we’ll also pass the -it
flag for interactive mode, so that we can more easily debug
any issues that might appear during setup of the database, and instruct the container to use
our new network and give it a descriptive DNS name.
podman run --name postgres -e POSTGRES_PASSWORD=password --volume=/media/psf/podman/postgres:/var/lib/postgresql/data --userns=keep-id:uid=999,gid=999 --network=development --hostname=postgres.localhost -p5432:5432 -it postgres:16
If there are no erros, stop the container again using Ctrl-C
, and then start it again. Also we
can check if we can access the server.
MAC ># podman start postgres
MAC ># psql -h postgres.localhost 5432 -U postgres
If this works, we can continue with the further setup steps.
Create development user and allow connections
In my setup I prefer to use a separate user for connecting from the Elixir apps,
instead of using postgres. We’ll use the createuser
tool for that.
MAC ># podman exec -it postgres bash
# in our example we'll use "phoenix" as the password as well
CONTAINER ># createuser -d -P phoenix
Next we’ll trust all connections inside our local development network. Note that this again is a step you should not be doing in production, but instead use a more restrictive setup there.
MAC ># podman stop postres
MAC ># vim ~/tmp/podman/postgres/pg_hba.conf
In the pg_hba.conf
file, add this line:
# podman network connections
host all all 10.0.0.1/8 trust
Restart the postgres container afterwards, and check if we can connect as the phoenix user:
MAC ># podman start postgres
MAC ># psql -h postgres.localhost -U phoenix postgres
Create a new Phoenix project with Ecto
For this we’ll remove our current container for development of Elixir projects, and recreate it using the new development network. Afterwards we’ll delete the old testing project and create a new one for testing.
MAC ># podman rm elixir_dev
# Note we've left out the security-opt label=disable option, since we've turned off SELinux
MAC ># podman run --name elixir_dev -p4000:4000 --volume=/media/psf/podman/testing:/src --network=development --hostname=elixir_dev.localhost -it elixir:1.16-otp-25 bash
CONTAINER ># cd /src
CONTAINER ># rm -rf *
CONTAINER ># rm -rf .*
CONTAINER ># mix local.hex --force
CONTAINER ># mix archive.install hex phx_new 1.7.11 --force
CONTAINER ># mix phx.new .
Again, make sure to adjust these settings in the config/dev.exs
file:
config :src, Src.Repo,
username: "phoenix",
password: "phoenix",
hostname: "postgres.localhost",
...
config :src, SrcWeb.Endpoint,
http: [ip: {0, 0, 0, 0}, port: 4000],
...
Then compile the project and run it:
CONTAINER ># mix deps.get
CONTAINER ># mix compile
CONTAINER ># mix ecto.setup
CONTAINER ># iex -S mix phx.server
And now to make sure everything has actually worked, we can try to connect to our new database from our Mac.
MAC ># psql -h postgres.localhost -U phoenix src_dev
Optional: Reimport your local databases
Finally we can reimport any of the exported databases as well:
MAC ># psql -h postgres.localhost -U phoenix postgres -c "CREATE DATABASE your_db;"
MAC ># psql -h postgres.localhost -U postgres your_db < your_db.sql
Conclusion and Outlook
So this was definitely the longest blog post I’ve written so far, and probably the most detailed 😎.
Up next I plan to add a post about how I prefer to create my Elixir development containers from a Dockerfile, and another one how I handle multiple different projects using only the single nginx server on the VM.