@@ -19,7 +19,7 @@ use crate::{DevContainerFeature, DevContainerSettings, DevContainerTemplate};
struct DevContainerUp {
_outcome: String,
container_id: String,
- _remote_user: String,
+ remote_user: String,
remote_workspace_folder: String,
}
@@ -156,6 +156,7 @@ pub async fn start_dev_container(
Ok(DevContainerUp {
container_id,
remote_workspace_folder,
+ remote_user,
..
}) => {
let project_name = match devcontainer_read_configuration(
@@ -180,6 +181,7 @@ pub async fn start_dev_container(
name: project_name,
container_id: container_id,
use_podman,
+ remote_user,
};
Ok((connection, remote_workspace_folder))
@@ -563,7 +565,7 @@ mod tests {
up.container_id,
"826abcac45afd412abff083ab30793daff2f3c8ce2c831df728baf39933cb37a"
);
- assert_eq!(up._remote_user, "vscode");
+ assert_eq!(up.remote_user, "vscode");
assert_eq!(up.remote_workspace_folder, "/workspaces/zed");
let json_in_plaintext = r#"[2026-01-22T16:19:08.802Z] @devcontainers/cli 0.80.1. Node.js v22.21.1. darwin 24.6.0 arm64.
@@ -574,7 +576,7 @@ mod tests {
up.container_id,
"826abcac45afd412abff083ab30793daff2f3c8ce2c831df728baf39933cb37a"
);
- assert_eq!(up._remote_user, "vscode");
+ assert_eq!(up.remote_user, "vscode");
assert_eq!(up.remote_workspace_folder, "/workspaces/zed");
}
}
@@ -33,6 +33,7 @@ use crate::{
pub struct DockerConnectionOptions {
pub name: String,
pub container_id: String,
+ pub remote_user: String,
pub upload_binary_over_docker_exec: bool,
pub use_podman: bool,
}
@@ -115,16 +116,39 @@ impl DockerExecConnection {
{
Ok(shell) => match shell.trim() {
"" => {
- log::error!("$SHELL is not set, falling back to {default_shell}");
- default_shell.to_owned()
+ log::info!("$SHELL is not set, checking passwd for user");
+ }
+ shell => {
+ return shell.to_owned();
+ }
+ },
+ Err(e) => {
+ log::error!("Failed to get $SHELL: {e}. Checking passwd for user");
+ }
+ }
+
+ match self
+ .run_docker_exec(
+ "sh",
+ None,
+ &Default::default(),
+ &["-c", "getent passwd \"$(id -un)\" | cut -d: -f7"],
+ )
+ .await
+ {
+ Ok(shell) => match shell.trim() {
+ "" => {
+ log::info!("No shell found in passwd, falling back to {default_shell}");
+ }
+ shell => {
+ return shell.to_owned();
}
- shell => shell.to_owned(),
},
Err(e) => {
- log::error!("Failed to get shell: {e}");
- default_shell.to_owned()
+ log::info!("Error getting shell from passwd: {e}. Falling back to {default_shell}");
}
}
+ default_shell.to_owned()
}
async fn check_remote_platform(&self) -> Result<RemotePlatform> {
@@ -370,44 +394,76 @@ impl DockerExecConnection {
Ok(())
}
- async fn upload_file(
- &self,
- src_path: &Path,
- dest_path: &RelPath,
- remote_dir_for_server: &str,
+ async fn upload_and_chown(
+ docker_cli: String,
+ connection_options: DockerConnectionOptions,
+ src_path: String,
+ dst_path: String,
) -> Result<()> {
- log::debug!("uploading file {:?} to {:?}", src_path, dest_path);
-
- let src_path_display = src_path.display().to_string();
- let dest_path_str = dest_path.display(self.path_style());
-
- let mut command = util::command::new_smol_command(self.docker_cli());
+ let mut command = util::command::new_smol_command(&docker_cli);
command.arg("cp");
command.arg("-a");
- command.arg(&src_path_display);
- command.arg(format!(
- "{}:{}/{}",
- &self.connection_options.container_id, remote_dir_for_server, dest_path_str
- ));
+ command.arg(&src_path);
+ command.arg(format!("{}:{}", connection_options.container_id, dst_path));
let output = command.output().await?;
+ if !output.status.success() {
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ log::debug!("failed to upload via docker cp {src_path} -> {dst_path}: {stderr}",);
+ anyhow::bail!(
+ "failed to upload via docker cp {} -> {}: {}",
+ src_path,
+ dst_path,
+ stderr,
+ );
+ }
+
+ let mut chown_command = util::command::new_smol_command(&docker_cli);
+ chown_command.arg("exec");
+ chown_command.arg(connection_options.container_id);
+ chown_command.arg("chown");
+ chown_command.arg(format!(
+ "{}:{}",
+ connection_options.remote_user, connection_options.remote_user,
+ ));
+ chown_command.arg(&dst_path);
+
+ let output = chown_command.output().await?;
+
if output.status.success() {
return Ok(());
}
let stderr = String::from_utf8_lossy(&output.stderr);
- log::debug!(
- "failed to upload file via docker cp {src_path_display} -> {dest_path_str}: {stderr}",
- );
+ log::debug!("failed to change ownership for via chown: {stderr}",);
anyhow::bail!(
- "failed to upload file via docker cp {} -> {}: {}",
- src_path_display,
- dest_path_str,
+ "failed to change ownership for zed_remote_server via chown: {}",
stderr,
);
}
+ async fn upload_file(
+ &self,
+ src_path: &Path,
+ dest_path: &RelPath,
+ remote_dir_for_server: &str,
+ ) -> Result<()> {
+ log::debug!("uploading file {:?} to {:?}", src_path, dest_path);
+
+ let src_path_display = src_path.display().to_string();
+ let dest_path_str = dest_path.display(self.path_style());
+ let full_server_path = format!("{}/{}", remote_dir_for_server, dest_path_str);
+
+ Self::upload_and_chown(
+ self.docker_cli().to_string(),
+ self.connection_options.clone(),
+ src_path_display,
+ full_server_path,
+ )
+ .await
+ }
+
async fn run_docker_command(
&self,
subcommand: &str,
@@ -440,6 +496,9 @@ impl DockerExecConnection {
None => vec![],
};
+ args.push("-u".to_string());
+ args.push(self.connection_options.remote_user.clone());
+
for (k, v) in env.iter() {
args.push("-e".to_string());
let env_declaration = format!("{}={}", k, v);
@@ -581,6 +640,8 @@ impl RemoteConnection for DockerExecConnection {
}
docker_args.extend([
+ "-u".to_string(),
+ self.connection_options.remote_user.to_string(),
"-w".to_string(),
self.remote_dir_for_server.clone(),
"-i".to_string(),
@@ -641,24 +702,14 @@ impl RemoteConnection for DockerExecConnection {
let dest_path_str = dest_path.to_string();
let src_path_display = src_path.display().to_string();
- let mut command = util::command::new_smol_command(self.docker_cli());
- command.arg("cp");
- command.arg("-a"); // Archive mode is required to assign the file ownership to the default docker exec user
- command.arg(src_path_display);
- command.arg(format!(
- "{}:{}",
- self.connection_options.container_id, dest_path_str
- ));
-
- cx.background_spawn(async move {
- let output = command.output().await?;
+ let upload_task = Self::upload_and_chown(
+ self.docker_cli().to_string(),
+ self.connection_options.clone(),
+ src_path_display,
+ dest_path_str,
+ );
- if output.status.success() {
- Ok(())
- } else {
- Err(anyhow::anyhow!("Failed to upload directory"))
- }
- })
+ cx.background_spawn(upload_task)
}
async fn kill(&self) -> Result<()> {
@@ -706,7 +757,11 @@ impl RemoteConnection for DockerExecConnection {
inner_program.push("-l".to_string());
};
- let mut docker_args = vec!["exec".to_string()];
+ let mut docker_args = vec![
+ "exec".to_string(),
+ "-u".to_string(),
+ self.connection_options.remote_user.clone(),
+ ];
if let Some(parsed_working_dir) = parsed_working_dir {
docker_args.push("-w".to_string());
@@ -1425,7 +1425,7 @@ impl WorkspaceDb {
options: RemoteConnectionOptions,
) -> Result<RemoteConnectionId> {
let kind;
- let mut user = None;
+ let user: Option<String>;
let mut host = None;
let mut port = None;
let mut distro = None;
@@ -1448,12 +1448,14 @@ impl WorkspaceDb {
kind = RemoteConnectionKind::Docker;
container_id = Some(options.container_id);
name = Some(options.name);
- use_podman = Some(options.use_podman)
+ use_podman = Some(options.use_podman);
+ user = Some(options.remote_user);
}
#[cfg(any(test, feature = "test-support"))]
RemoteConnectionOptions::Mock(options) => {
kind = RemoteConnectionKind::Ssh;
host = Some(format!("mock-{}", options.id));
+ user = Some(format!("mock-user-{}", options.id));
}
}
Self::get_or_create_remote_connection_query(
@@ -1691,6 +1693,7 @@ impl WorkspaceDb {
Some(RemoteConnectionOptions::Docker(DockerConnectionOptions {
container_id: container_id?,
name: name?,
+ remote_user: user?,
upload_binary_over_docker_exec: false,
use_podman: use_podman?,
}))